Skip to content

fix(cli): pass granular Claude SDK runtime options#94

Draft
rawwerks wants to merge 2 commits into
mainfrom
feat/cli-claude-permission-mode
Draft

fix(cli): pass granular Claude SDK runtime options#94
rawwerks wants to merge 2 commits into
mainfrom
feat/cli-claude-permission-mode

Conversation

@rawwerks
Copy link
Copy Markdown
Contributor

PR Body: fix(cli) — pass granular Claude SDK runtime options

Ready-to-paste body for gh pr create --body-file notes/003-pr-body.md
against branch feat/cli-claude-permission-modemain on
openprose/prose. Commits: d4a3e3e (patch) and d312c7f (audit
fix-ups).


Summary

  • Add PROSE_CLAUDE_PERMISSION_MODE env-var passthrough to the
    claude-sdk harness, mirroring the existing PROSE_CODEX_* pattern
    added in fix(cli): pass granular Codex runtime options #77/fix(cli): pass granular Codex runtime options #78.
  • Validate against the SDK's permission-mode union
    {default, acceptEdits, bypassPermissions, plan} and forward the
    validated value to @anthropic-ai/claude-agent-sdk's query() as
    permissionMode.
  • Unblocks non-interactive prose run --harness claude-sdk in CI,
    conformance benchmarks, and scheduled jobs that previously stalled
    because permissionMode is a query-level SDK option that
    settingSources does not flow into.
  • Three vitest cases (forward / omit / reject-invalid); one README
    block alongside the codex env-var section; one [Unreleased]
    CHANGELOG entry.

Use Case / Run Evidence

Discovered while building a telltail-backed prose conformance
benchmark (private workbench: rawwerks/telltail-prose-lab). The
benchmark drives the upstream tests/open-prose/smoke/ suite across
(harness, model) cells and scores each via a deterministic
sentinel-substring check on runs/{id}/bindings/**.

Friction: under default config, prose run --harness claude-sdk could
not complete a single smoke fixture in non-interactive mode because
the SDK fell back to permissionMode: "default" and prompted for
explicit Write approval on every binding/run-receipt write. Three
independent reproductions (manual probe, manual probe with
project-level .claude/settings.json bypassPermissions, orchestrator
attempt) all hit the same gate. codex-sdk had no equivalent friction
because PROSE_CODEX_SANDBOX_MODE / PROSE_CODEX_APPROVAL_POLICY
already wired through.

Concrete prose runs (in the workbench, gitignored):

  • Pre-patch: every claude-sdk cell in outcomes/v1.jsonl records
    failure_mode: "permission_gate_blocked_writes" (3 fixtures × 3
    reproductions).
  • Post-patch: runs/20260522-195404-016210/ with
    PROSE_CLAUDE_PERMISSION_MODE=bypassPermissions — a real
    claude-sdk run that wrote root.prose.md, vm.log.md, source
    snapshots, workspace output, and the declared message binding
    containing the fixture's sentinel string. Scorer returns score=1.0
    with sentinels_found={"single-service-smoke-pass": true},
    failure_mode: null.

Design Boundary

The change is entirely in tools/cli/src/harnesses/. It does not move
any semantics into the CLI: prose-cli still does no permission decision
of its own — it reads a single env var, validates its value, and
forwards the SDK option. Per CONTRIBUTING.md's "Where Changes Belong,"
this is the row for "Shell entrypoint, harness forwarding."

No skill files, no spec docs, no packages/std/, no packages/co/,
no examples touched. No language/framework surface modified.

Naming, validation shape, error-message format, and return-type
discipline all mirror codex-options.ts so a maintainer reviewing both
files sees a uniform pattern.

Examples

# Default (interactive): SDK keeps prompting for permission, same as today.
prose run service.prose.md --harness claude-sdk

# Non-interactive / CI / conformance run:
PROSE_CLAUDE_PERMISSION_MODE=bypassPermissions \
  prose run service.prose.md --harness claude-sdk

# Plan-only (e.g. preflight in CI before a deliberate write step):
PROSE_CLAUDE_PERMISSION_MODE=plan \
  prose run service.prose.md --harness claude-sdk

Invalid values throw with the same error shape as codex:

Error: PROSE_CLAUDE_PERMISSION_MODE must be one of: default,
acceptEdits, bypassPermissions, plan

Testing

cd tools/cli
pnpm install --frozen-lockfile
pnpm exec vitest run tests/harnesses/harnesses.test.ts
#   → 16 tests pass (13 existing + 3 new)

The three new cases:

  1. forwards PROSE_CLAUDE_PERMISSION_MODE to the SDK as permissionMode
    — given env: { PROSE_CLAUDE_PERMISSION_MODE: "bypassPermissions" },
    the query options receive permissionMode: "bypassPermissions".
  2. omits permissionMode when PROSE_CLAUDE_PERMISSION_MODE is unset
    given env: {}, the query options do not have a permissionMode
    key.
  3. rejects invalid PROSE_CLAUDE_PERMISSION_MODE — given
    env: { PROSE_CLAUDE_PERMISSION_MODE: "yolo" }, the harness throws
    the expected must be one of error before calling the SDK.

End-to-end verification beyond unit tests:

  • Built the patched dist (pnpm --filter @openprose/prose-cli build).
  • Drove it from a real tmux shell against the upstream
    01-single-service.prose.md smoke fixture with
    PROSE_CLAUDE_PERMISSION_MODE=bypassPermissions. The SDK fired
    (claude-agent-sdk@0.2.141 resolved via pnpm), produced
    runs/20260522-195404-016210/ with the full filesystem run envelope,
    and the binding contained the fixture's sentinel.
  • Negative E2E: same invocation with PROSE_CLAUDE_PERMISSION_MODE=yolo
    surfaced the validator's exact error string through prose-cli's
    standard error prefix — proving the patched dist (not some other
    path) executed.

Residual Risk / Follow-ups

  • The wider pnpm run typecheck reports pre-existing errors in
    src/prose/repository-serve-reactor-adapters.ts,
    src/prose/repository-status.ts,
    src/prose/responsibility-reactor.ts, and several files referencing
    zod 4 / bun-types. These are identical on origin/main and not
    introduced by this PR. The narrow vitest run tests/harnesses/harnesses.test.ts is clean and is the test the
    CONTRIBUTING.md "CLI or harness behavior" row points at.
  • E2E was performed via a Claude Code-style host driving the SDK; a
    fully independent Anthropic API run (different account, different
    model preset) wasn't tested. The wrapper code is harness-shape, not
    model-shape, so this remains the correct surface area; flagging for
    transparency.
  • permissionMode precedence between runOptions.env and
    process.env follows the same env?.[name] ?? process.env[name]
    pattern as codex-options.ts (claude-options.ts:21). Not
    separately tested — neither is the codex case — but the precedence
    matches the codex precedent so consumers can rely on the same mental
    model across both harnesses.
  • Documentation note: tools/cli/AGENTS.md does not exist; this PR
    does not create one. If a future PR adds an agents file for the CLI,
    the env vars belong there.

🤖 Generated with Claude Code

rawwerks added 2 commits May 22, 2026 15:38
Mirror the codex env-var pattern (PROSE_CODEX_SANDBOX_MODE,
PROSE_CODEX_APPROVAL_POLICY, ...) on the claude-sdk side so non-
interactive harness runs can widen the SDK's default permission gate.

Background. PR #70 set settingSources: ["user", "project"] so the SDK
reads ~/.claude and <repo>/.claude, but permissionMode is a query-level
option that settings.json does not flow into. With the wrapper never
passing permissionMode, the SDK falls back to "default" and prompts
for explicit Write approval on every tool use -- making claude-sdk
unrunnable in CI, conformance benchmarks, or any other context that
cannot answer prompts. codex-sdk got env-var passthrough for the
equivalent knobs; claude-sdk did not.

Adds tools/cli/src/harnesses/claude-options.ts with claudeRuntimeOptions(env)
reading PROSE_CLAUDE_PERMISSION_MODE and validating against
{default, acceptEdits, bypassPermissions, plan}. claude-sdk.ts spreads
the result into the query() options.

Tests (vitest):
- forwards PROSE_CLAUDE_PERMISSION_MODE to the SDK as permissionMode
- omits permissionMode when the env var is unset
- rejects invalid values with the same error shape as codex-options

README documents the new env var alongside the codex section.
Three should-fix items surfaced by an independent audit against
CONTRIBUTING.md:

- CHANGELOG: add [Unreleased] entry naming PROSE_CLAUDE_PERMISSION_MODE
  so the env var surfaces in release notes (codex env vars missed this
  in their original PR; not repeating the omission here).
- claude-options.ts: tighten return type to
  Pick<ClaudeQueryOptions, "permissionMode"> against the Claude SDK's
  own query() options type. If the SDK ever changes its permissionMode
  union (adds/removes a mode), this surfaces at compile time instead of
  silently drifting. Mirrors codex-options.ts's
  Pick<CodexThreadOptions, ...> shape.
- harnesses.test.ts: pass explicit `env: {}` in the "omit when unset"
  test so it stops depending on the host shell's environment. Without
  this, running the test suite with PROSE_CLAUDE_PERMISSION_MODE
  exported (as during the E2E spike that motivated this PR) made the
  test flake.

Re-verified: 16/16 vitest tests pass; E2E validator still throws
expected error on PROSE_CLAUDE_PERMISSION_MODE=yolo after rebuild.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant