Repository navigation
fix(native-chat): refuse a Command that can't run instead of silently running the stock CLI - #25667
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (6)
🚧 Files skipped from review as they are similar to previous changes (6)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review. 📝 WalkthroughWalkthroughClaude and Codex structured launches now resolve configured command overrides, validate their arguments, and pass invocation details to spawned processes. Model-catalog probes use prepared or session-pinned invocations and include command, prefix arguments, and cwd in catalog identity. Command-related invalid and conflicting launches receive typed failure classifications and localized UI messages. Structured routing requires a terminal for a cwd outside the workspace root, not solely for a command override. Priority: ➖ Normal Merge Risk: 🔵 Low · up to Custom-command failure messages appear aligned across the six locales. Windows users whose environment sets both HOME and USERPROFILE may still have a tilde-prefixed command resolved against the wrong directory; this is a bounded risk to address or accept before merging. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to Custom commands now run during native chat and model discovery. Host ownership checks, argument restrictions, and command-specific caching limit unintended effects. No security violation was established, but configured programs remain trusted local code and their behavior cannot be guaranteed. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 37.14% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 35 functions across 54 files. (6 skipped: 6 unsupported.)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
d397130a-7f33-4690-ad31-f1049e76b998
📒 Files selected for processing (78)
src/main/agent-launch/agent-launch-executor.tssrc/main/agent-launch/agent-launch-mode.tssrc/main/claude/claude-agent-sdk-contract-pins.test.tssrc/main/claude/claude-agent-sdk-process-spawn.test.tssrc/main/claude/claude-agent-sdk-process-spawn.tssrc/main/claude/claude-model-catalog-probe.test.tssrc/main/claude/claude-model-catalog-probe.tssrc/main/claude/claude-stream-json-connection.test.tssrc/main/claude/claude-stream-json-connection.tssrc/main/claude/claude-structured-launch-resolution.test.tssrc/main/claude/claude-structured-launch-resolution.tssrc/main/claude/claude-structured-session-acquisition-processless.test.tssrc/main/claude/claude-structured-session-acquisition.tssrc/main/claude/claude-structured-session-adapter.tssrc/main/codex-cli/command.test.tssrc/main/codex/codex-app-server-session.test.tssrc/main/codex/codex-app-server-session.tssrc/main/codex/codex-model-catalog-probe.test.tssrc/main/codex/codex-model-catalog-probe.tssrc/main/codex/codex-structured-acquire-catalog.tssrc/main/codex/codex-structured-launch-resolution.test.tssrc/main/codex/codex-structured-launch-resolution.tssrc/main/codex/codex-structured-session-adapter.tssrc/main/codex/codex-structured-session-state.tssrc/main/native-chat/agent-model-catalog/agent-model-catalog-fingerprint.tssrc/main/native-chat/agent-model-catalog/agent-model-catalog-service.test.tssrc/main/native-chat/agent-model-catalog/agent-model-catalog-service.tssrc/main/native-chat/agent-model-catalog/agent-model-catalog-store.tssrc/main/native-chat/agent-session-wire/structured-agent-session-adapter.tssrc/main/native-chat/agent-session-wire/structured-agent-session-failure-text.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-failure-text.tssrc/main/native-chat/agent-session-wire/structured-agent-session-pre-spawn-first-answer.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-start-failure-writer.test.tssrc/main/native-chat/structured-agent-command-resolution.test.tssrc/main/native-chat/structured-agent-command-resolution.tssrc/main/native-chat/structured-agent-session-create-support.test.tssrc/main/native-chat/structured-agent-session-create-support.tssrc/main/runtime/orca-runtime-get-worktree-ps.tssrc/main/runtime/rpc/methods/orchestration-worker-start-mode.test.tssrc/main/runtime/rpc/methods/orchestration-worker-start-receipt-wording.test.tssrc/main/runtime/structured-agent-model-catalog-wiring.tssrc/main/runtime/structured-agent-session-runtime.tssrc/main/runtime/structured-claude-runtime-adapter.tssrc/renderer/src/components/native-chat/NativeChatLaunchRetry.command-failure.test.tsxsrc/renderer/src/components/native-chat/agent-session-failure-words-text.tssrc/renderer/src/components/native-chat/native-chat-session-option-discovery.tssrc/renderer/src/components/native-chat/native-chat-session-option-enrichment.test.tssrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/i18n/locales/es.jsonsrc/renderer/src/i18n/locales/fr.jsonsrc/renderer/src/i18n/locales/ja.jsonsrc/renderer/src/i18n/locales/ko.jsonsrc/renderer/src/i18n/locales/zh.jsonsrc/renderer/src/lib/agent-launch-route-input.test.tssrc/renderer/src/lib/agent-launch-route-input.tssrc/renderer/src/lib/agent-launch-routing.test.tssrc/renderer/src/lib/agent-launch-routing.tssrc/shared/agent-launch-intent.tssrc/shared/agent-session-failure-copy.tssrc/shared/agent-session-failure-words.tssrc/shared/agent-session-failure.tssrc/shared/agent-session-refusal-details.tssrc/shared/agent-session-refusal-reason-words.tssrc/shared/node-cli-command-resolution.tssrc/shared/structured-agent-session-dispatch-rejection.tssrc/shared/structured-native-chat-launch-route.test.tssrc/shared/structured-native-chat-launch-route.tssrc/shared/tui-agent-launch-command-override.test.tssrc/shared/tui-agent-launch-command-override.tstests/e2e/completed-worker-retirement-resume.spec.tstests/e2e/helpers/golden-stub-agent.tstests/e2e/orchestration-legacy-worker-missing-terminal-recovery.spec.tstests/e2e/orchestration-legacy-worker-restart-recovery.spec.tstests/e2e/orchestration-worker-settlement-release-cli.spec.tstests/e2e/orchestration-worker-terminal-visibility.spec.tstests/e2e/orchestration-worker-transcript-providers.spec.tstests/e2e/settled-worker-tab-survives-restart.spec.tstests/e2e/structured-native-chat-routing-authority.unit.test.ts
💤 Files with no reviewable changes (1)
- src/renderer/src/lib/agent-launch-routing.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 4 remain after this review.
| return null | ||
| } | ||
| const api = platform === 'win32' ? path.win32 : path.posix | ||
| const home = input.env.HOME ?? input.env.USERPROFILE ?? homedir() |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
sed -n '1,90p' src/main/native-chat/structured-agent-command-resolution.ts
rg -n 'USERPROFILE|HOME.*homedir|homedir.*USERPROFILE' src/main/claude/claude-structured-launch-resolution.ts src/main/codex/codex-structured-launch-resolution.ts src/shared/node-cli-command-resolution.tsRepository: stablyai/orca
Length of output: 3429
🏁 Script executed:
printf '%s\n' '--- PR diff ---'
git diff --no-ext-diff --unified=5 b354aab18e1a691cedb7f40f7579ef7e9b9a6f17 4e4d990ecf2032d94413fd66cdce773f546a1de2 -- src/main/native-chat/structured-agent-command-resolution.ts
printf '%s\n' '--- resolver call sites ---'
rg -n -F -- 'resolveStructuredAgentCommand(' src
rg -n -F -- 'customCommandInvalid' src/main
printf '%s\n' '--- structured command spawn/call consumers ---'
rg -n 'StructuredAgentCommandInvocation|prefixArgs|spawn\(' src/main/native-chat src/main/claude src/main/codex
printf '%s\n' '--- homePath policy references ---'
sed -n '190,245p' src/main/claude/claude-structured-launch-resolution.ts
sed -n '35,85p' src/main/codex/codex-structured-launch-resolution.ts
rg -n -F -- 'homePath:' src/main src/sharedRepository: stablyai/orca
Length of output: 41167
🏁 Script executed:
printf '%s\n' '--- Claude resolver imports/types/environment setup ---'
sed -n '1,100p' src/main/claude/claude-structured-launch-resolution.ts
sed -n '120,245p' src/main/claude/claude-structured-launch-resolution.ts
printf '%s\n' '--- Claude resolver construction and launch ---'
rg -n -F -- 'createClaudeStructuredLaunchResolver' src/main
rg -n -F -- 'inheritedEnv' src/main/claude src/main/startup
sed -n '1,115p' src/main/claude/claude-agent-sdk-process-spawn.ts
printf '%s\n' '--- Codex resolver deps and launch ---'
sed -n '1,150p' src/main/codex/codex-structured-launch-resolution.ts
rg -n -F -- 'resolveEnvironment:' src/main/codex src/main/startup
printf '%s\n' '--- environment helpers and host HOME setup ---'
rg -n 'function decorateEnv|const decorateEnv|decorateEnv\s*=|USERPROFILE\s*=|HOME\s*=' src/main src/shared
printf '%s\n' '--- command-resolution tests around windows and HOME ---'
sed -n '1,115p' src/main/native-chat/structured-agent-command-resolution.test.ts
printf '%s\n' '--- home selection precedent in launch resolver ---'
rg -n 'baseEnv\.HOME \?\? baseEnv\.USERPROFILE|environment\?\.HOME \?\? environment\?\.USERPROFILE|HOME.*USERPROFILE' src/main/claude src/main/codex src/main/native-chat src/sharedRepository: stablyai/orca
Length of output: 42102
🏁 Script executed:
printf '%s\n' '--- Claude runtime adapter wiring ---'
sed -n '55,110p' src/main/runtime/structured-claude-runtime-adapter.ts
sed -n '255,340p' src/main/claude/claude-structured-launch-resolution.ts
printf '%s\n' '--- Claude environment provider bindings ---'
rg -n 'resolveInheritedEnv|resolveEnv:|decorateEnv' src/main/runtime src/main/claude
printf '%s\n' '--- Codex environment provider bindings ---'
rg -n 'resolveEnvironment|resolveCommandOverride|resolveCodexStructuredInvocation' src/main/runtime src/main/codex src/main/startup
printf '%s\n' '--- Codex launch invocation and environment ---'
sed -n '95,145p' src/main/codex/codex-structured-launch-resolution.ts
rg -n -F -- 'invocation.command' src/main/codex
printf '%s\n' '--- Windows process spawn command and environment contract ---'
rg -n -F -- 'function createProviderSpawnSpec' src/main/codex
sed -n '1,130p' src/main/codex/codex-app-server-posix-supervisor.ts
printf '%s\n' '--- CLI home-path resolver contract ---'
sed -n '1,115p' src/shared/node-cli-command-resolution.ts
sed -n '115,180p' src/shared/node-cli-command-resolution.tsRepository: stablyai/orca
Length of output: 32995
🏁 Script executed:
printf '%s\n' '--- structured runtime dependency types and wiring ---'
sed -n '80,135p' src/main/runtime/structured-agent-session-runtime.ts
sed -n '235,305p' src/main/runtime/structured-agent-session-runtime.ts
rg -n 'createStructuredAgentSessionRuntime|resolveClaudeInheritedEnv|resolveClaudeLaunchEnv|resolveCodexEnvironment' src/main
printf '%s\n' '--- main process runtime service environment setup ---'
sed -n '1,95p' src/main/startup/main-process-runtime-service.ts
printf '%s\n' '--- shared environment store ---'
sed -n '1,240p' src/shared/runtime-environment-store.ts
printf '%s\n' '--- shell environment construction ---'
sed -n '55,145p' src/main/runtime/structured-agent-shell-environment.ts
rg -n 'resolveLoginShellEnvironment' src/main/startup src/main/runtime src/shared
printf '%s\n' '--- Windows spawn spec ---'
sed -n '165,245p' src/main/codex/codex-app-server-posix-supervisor.ts
printf '%s\n' '--- Codex app server process call ---'
sed -n '95,135p' src/main/codex/codex-app-server-session.tsRepository: stablyai/orca
Length of output: 30294
🏁 Script executed:
printf '%s\n' '--- shell env selection and merge ---'
sed -n '1,75p' src/main/runtime/structured-agent-shell-environment.ts
rg -n 'function nativeChatShellEnvironmentPolicy|nativeChatShellEnvironmentPolicy' src/main src/shared
printf '%s\n' '--- login shell environment Windows path ---'
sed -n '115,205p' src/main/startup/login-shell-environment.ts
printf '%s\n' '--- runtime env resolver wiring ---'
rg -n 'createStructuredAgentSessionRuntime|resolveLaunchEnvOverlay|resolveCodexOverrides|resolveEnvironment:' src/main/runtime/orca-runtime.ts src/main/runtime
printf '%s\n' '--- structured command parser ---'
rg -n 'function parseStructuredAgentCommandOverride|parseStructuredAgentCommandOverride' src/shared/tui-agent-launch-command-override.ts
sed -n '1,135p' src/shared/tui-agent-launch-command-override.ts
printf '%s\n' '--- process spawn Windows handling ---'
rg -n 'spawnCodexAppServerProcess|createProviderSpawnSpec|cmd\.exe|windowsVerbatimArguments' src/shared/child-process src/main/codex
sed -n '1,220p' src/shared/child-process/run-process.ts
rg -n 'spawnProcess|createProviderSpawnSpec' src/main/codex/codex-app-server-session.ts src/main/codex/codex-app-server-connection.tsRepository: stablyai/orca
Length of output: 35739
🏁 Script executed:
printf '%s\n' '--- command resolver ---'
nl -ba src/main/native-chat/structured-agent-command-resolution.ts | sed -n '18,70p'
printf '%s\n' '--- host environment construction ---'
nl -ba src/main/runtime/structured-agent-shell-environment.ts | sed -n '1,112p'
nl -ba src/shared/native-chat-shell-environment.ts | sed -n '1,75p'
printf '%s\n' '--- Claude env transform ---'
rg -n 'function applyClaudeEnvPatch|export function applyClaudeEnvPatch' src/main/claude-accounts/environment.ts
nl -ba src/main/claude-accounts/environment.ts | sed -n '55,135p'
printf '%s\n' '--- existing default-home precedence ---'
nl -ba src/main/claude/claude-structured-launch-resolution.ts | sed -n '195,230p'
nl -ba src/main/codex/codex-structured-launch-resolution.ts | sed -n '52,80p'
printf '%s\n' '--- resolver and spawn chain ---'
nl -ba src/main/claude/claude-agent-sdk-process-spawn.ts | sed -n '50,85p'
nl -ba src/main/codex/codex-app-server-connection.ts | sed -n '55,85p'
nl -ba src/main/codex/codex-app-server-posix-supervisor.ts | sed -n '176,200p'
nl -ba src/shared/child-process/spawn-resolution.ts | sed -n '25,85p'
printf '%s\n' '--- runtime reachability guards ---'
nl -ba src/main/claude/claude-structured-launch-resolution.ts | sed -n '262,325p'
nl -ba src/main/codex/codex-structured-launch-resolution.ts | sed -n '94,132p'Repository: stablyai/orca
Length of output: 27532
🏁 Script executed:
printf '%s\n' '--- Claude env patch exact body ---'
nl -ba src/main/claude-accounts/environment.ts | sed -n '1,52p'
printf '%s\n' '--- inherited config-dir handling ---'
nl -ba src/main/claude/claude-config-dir-pin.ts | sed -n '1,90p'
printf '%s\n' '--- Claude child identity decoration ---'
nl -ba src/main/runtime/structured-session-child-identity-env.ts | sed -n '1,90p'
printf '%s\n' '--- Windows command tokenizer path ---'
rg -n 'function tokenizeStartupCommand|tokenizeStartupCommand' src/shared/tui-agent-startup-shell.ts
nl -ba src/shared/tui-agent-startup-shell.ts | sed -n '1,125p'Repository: stablyai/orca
Length of output: 17039
Use USERPROFILE for Windows ~\\ expansion.
When a Windows login-shell environment sets HOME to an MSYS-style path and USERPROFILE points to the Windows profile, the resolver expands ~\\wrapper.cmd from HOME. The file check can then fail with customCommandInvalid. Keep the existing HOME-first value for bare-command lookup.
Suggested fix
const home = input.env.HOME ?? input.env.USERPROFILE ?? homedir()
+ const tildeHome = platform === 'win32' ? input.env.USERPROFILE ?? home : home
let command = invocation.command
if (command.startsWith('~/') || (platform === 'win32' && command.startsWith('~\\'))) {
- command = api.join(home, command.slice(2))
+ command = api.join(tildeHome, command.slice(2))There was a problem hiding this comment.
Important
Native chat now honors the custom Command, but the model-catalog cache key drops cwd for a default invocation while the probe runs in the workspace directory, so two workspaces can be served each other's listing. Two command-validator gaps are also worth a look.
Reviewed changes
- Shared command parser/validator (
tui-agent-launch-command-override.ts): tokenizes the Command, rejects shell evaluation, and rejects transport/account-owning flags plus model/effort conflicts. - Host-side resolution (
structured-agent-command-resolution.ts): resolves the program against host env/cwd, checks executability, and rejects shells, raisingAgentSessionPreSpawnErrorwithcustomCommandInvalid/customCommandConflict. - Provider launches: Claude and Codex prepend the wrapper's prefix args; the Claude SDK spawn callback strips the SDK's inserted script path, and Codex app-server receives
cwdon the spawn. - Routing:
requiresTuiLaunchCommandnow means only "cwd outside the workspace root";hasExplicitTuiLaunchCommandis deleted and worker-start receipts reworded. - Model catalog: fingerprints include the invocation, live sessions pin theirs, and the renderer sends
worktreeso the host probes in the workspace. - Failure UX: new
customCommandInvalid/customCommandConflictcopy in six locales, plus e2e specs explicitly disabling structured chat where a stub command is used.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
| (params.agent === 'claude' ? 'CLAUDE_CONFIG_DIR' : 'CODEX_HOME'), | ||
| accountHomePath, | ||
| wslDistro: scoped?.location.wslDistro ?? null, | ||
| invocation: prepared.invocation |
There was a problem hiding this comment.
The fingerprint built here from prepared.invocation omits cwd whenever no custom Command is set: resolveStructuredAgentCommand returns null early, so neither provider's resolve*StructuredInvocation ever attaches cwd, yet the probe is spawned in the workspace cwd. Two session-less reads (or two live default sessions) in different workspaces on the same account therefore share one cache entry and can be served each other's listing.
Technical details
# Default-invocation catalog key omits the probe's working directory
## Affected sites
- `src/main/native-chat/agent-model-catalog/agent-model-catalog-service.ts:150` — keys the entry by `prepared.invocation`, which has no `cwd` for a default command.
- `src/main/native-chat/structured-agent-command-resolution.ts:22-24,55-58` — returns `null` before attaching `cwd` when there is no override.
- `src/main/claude/claude-structured-launch-resolution.ts:227-233` and `src/main/codex/codex-structured-launch-resolution.ts:75` — spread `selected` (null) and never set `cwd`.
- `src/main/runtime/structured-agent-model-catalog-wiring.ts:84-124` — passes the workspace `cwd` into `prepareProbe`, and the probes spawn in it.
## Required outcome
- The cache key must include the directory the probe actually runs in, for default commands too — otherwise a listing probed in workspace A is returned for workspace B.
## Suggested approach (optional)
- Echo `cwd` onto the invocation unconditionally in both `resolve*StructuredInvocation` (not only when `selected` is non-null), or fold the `cwd` passed to `prepareProbe` into the fingerprint independently of the invocation.| args.some((arg) => /^(CODEX_HOME|CLAUDE_CONFIG_DIR)=/.test(arg)) || | ||
| removeAgentArgOption(agent, args, owned).length !== args.length || | ||
| (agent === 'codex' && | ||
| removeAgentArgOption(agent, args, ['-c', '--config'], (value) => |
There was a problem hiding this comment.
The Codex -c/--config predicate is meant to reject config that takes over the sandbox/approval/account posture, but it matches only five exact keys and is anchored at ^ on the raw token. A quoted -c ' sandbox_mode=danger-full-access' survives (the leading space defeats the regex, and Codex trims the key before applying), and -c sandbox_workspace_write.network_access=true is not matched at all.
Technical details
# Codex `-c` config-key rejection is bypassable
## Affected sites
- `src/shared/tui-agent-launch-command-override.ts:116-121` — the `-c`/`--config` value predicate.
## Required outcome
- Reject any `-c`/`--config` value whose key (trimmed) names a transport/sandbox/approval/account setting, including the `sandbox_workspace_write.*` table.
## Suggested approach (optional)
- Take `value.trim().split('=', 1)[0]` and match a prefix/allowlist rather than an anchored exact-key regex.| .basename(command) | ||
| .replace(/\.exe$/i, '') | ||
| .toLowerCase() | ||
| if (['sh', 'bash', 'zsh', 'fish', 'cmd', 'powershell', 'pwsh', 'env'].includes(shell)) { |
There was a problem hiding this comment.
The shell blocklist only inspects the resolved binary's basename, so dash/ksh/ash and launchers such as sudo sh -c or nice sh -c pass. dash is /bin/sh on Debian/Ubuntu, so the intended "shell-only commands fail" guarantee has a common hole.
Technical details
# Shell blocklist is incomplete
## Affected sites
- `src/main/native-chat/structured-agent-command-resolution.ts:48-54` — basename-only shell check.
## Required outcome
- A command that is (or wraps) a shell which evaluates its argument should be refused with `customCommandInvalid`, on both POSIX and Windows.
## Open questions for the human (optional)
- Is this list intended as best-effort? If so, a comment saying so would set expectations; if not, add the sh-family and launcher cases (or inspect argv beyond `argv[0]`).There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Re-reviewed only the delta since the prior pullfrog review (commit 4e4d990): the new head is a sync merge of origin/main, so there is no new PR-authored behavior — just conflict resolution in the shared files both sides touched.
- Merged
origin/maininto the branch (c127b05merges4e4d990+9def4b9); the large upstream delta is required context, not authored work. - Reconciled the Claude launch path: main's extracted
claudeAgentSdkQueryOptions, singlecwdvariable, and provider-handle leaf encoding now sit beside this PR's custom-commandinvocation/env, and theinvocationstill reaches the SDK spawn callback. - Reconciled Codex: upstream's provider-record-reader and session-state renames merged without dropping the custom-command
prefixArgsor theapp-serverlaunch. - Reconciled failure vocabularies: main's
providerStartFailedand this PR'scustomCommandInvalid/customCommandConflictcoexist in every registry (shared words, refusal reasons, dispatch-rejection map, six locale catalogs). - Verified the feature still wires end to end with focused suites at the merged head; the only failure (
claude-stream-json-connection.test.ts, descendant-cleanup timing) reproduces identically onmainand is pre-existing.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…hat-launch-command
…hat-launch-command
… program Native chat ignored the Command setting, so a user who pointed it at a custom Claude or Codex build still got the stock CLI. The host now reads the Command per acquisition and per model-catalog probe, resolves it on the execution host as a program (an absolute path, ~ path, or a name on PATH), and spawns it with the normal structured arguments. A set Command that is not a runnable program refuses the start with a new agentCommandNotRunnable failure that names the setting; it never falls back to the stock CLI.
…; clearer copy A Claude Command given as a bare name was looked up on Orca's own PATH, not the PATH the Claude child launches with (login shell plus Settings → Agents environment), so a name only that environment provides was refused. Codex already resolved against its launch environment. Claude now reads its launch env first and resolves a configured name against its PATH and home; the stock lookup with no Command is unchanged. The failure copy now states the rule (a program path or name, no arguments or variables) and names the Settings control (Reset). The catalog fingerprint doc notes that the program is not keyed, so a chat on an older program keeps refreshing the shared entry until it ends.
… Windows On Windows, an inherited `Path` and a Settings → Agents env `PATH` both reached the Claude child. The configured-program lookup read the first inserted twin (inherited), while Node's child_process keeps the lexicographically first (`PATH`, the user's), so a bare-name Command found only on the Settings PATH was refused. The Claude launch env now drops inherited case-twins of the overlay's variables (the rule the structured shell env already applies), so the lookup and the child read one PATH. pathEnvOf follows Node's win32 key rule, and Codex's lookup uses it too, so both agents read PATH one way. es/fr/ja copy now says the Command must be, as the English does.
Review summary (final head 8e48f0f, a follow-up to #25721)The problem. Since #25721, native chat runs the program in Settings → Agents → Command. But when that Command couldn't be run as a program (a typo'd or removed path, a non-executable file, or a whole command line like What changes for users.
How it was built. This PR was first written before #25721 and was on hold after #25721 landed with the opposite rule. Brennan chose the refusal, which is the common pattern: an explicitly configured program that can't run is an error naming the setting, never a silent swap. The branch was reset to main through a merge commit (no force-push) and rebuilt as the smallest change to #25721's shared resolver: a set Command that doesn't resolve throws a typed Fixed in review.
Considered and dropped: a different Claude PATH lookup and a different Deferred (not this PR).
Verified.
Not verified.
|
…the stock CLI A saved Settings → Agents → Command that did not resolve to a program (a command line such as `codex --profile work`, `FOO=1 claude` or `npx …`, a missing or relative path, a non-executable file) silently ran the stock Claude/Codex in native chat and its model-catalog probe. The user's choice was ignored with no sign of it. A set Command that does not resolve now refuses the start with a new agentCommandNotRunnable failure naming the setting, beside the existing Retry; the catalog probe refuses the same way and never lists the stock CLI. A blank Command keeps the stock lookup. On Windows a resolved file without a spawnable extension (.exe/.com/.cmd/.bat) refuses too, instead of failing to spawn with no word about the setting.
… Command After #25721 native chat runs a runnable Command, and this PR refuses one it can't run; four comments still said the Command applies to terminal launches only. The pre-spawn test fixture also drops the saved value from its error message, matching the resolver.

ELI5
In Settings → Agents you can set a Command for Claude or Codex: the program Orca runs for that agent. Since #25721, native chat runs that program too. But when the Command can't be run as a program (a typo'd path, a removed binary, or a whole command line like
claude --verboseorFOO=1 claude), native chat quietly started the stockclaude/codexon your PATH instead. You'd be chatting with a different program than the one you picked, with nothing telling you.Now native chat refuses that start and tells you what to fix: "Claude's Command in Settings → Agents must be a program path or name Orca can find, with no arguments or variables. Change it or reset it." with a Retry button. An empty Command still runs the stock CLI, as before.
What Changed
The problem.
resolveStructuredAgentCommand(from #25721) resolves the Command for both a native chat and its model-list check. When the Command didn't resolve to a runnable program, it returned the stock CLI instead. So a broken or command-line Command silently ran a different binary in native chat, in its model list, and in native orchestration workers.Before → after, as a user:
/opt/tools/claude-nightly,~/bin/codex-beta,"C:\Program Files\Claude\claude.exe", a name on your PATH): unchanged from fix(native-chat): honor saved agent Command and Arguments #25721. That program runs, with your saved Arguments.FOO=1 claude,claude --verbose,npx @anthropic-ai/claude-code,codex --profile work) now see this message in native chat instead of having the setting silently ignored. Their terminal launches are unchanged. The fix is to put just the program in Command, and supported flags in Arguments.Mechanism.
resolveStructuredAgentCommand(structured-agent-command-resolution.ts): a blank Command uses the stock lookup. A set Command that fix(native-chat): honor saved agent Command and Arguments #25721's existing tokenizer and resolver can't turn into a runnable program throws a pre-spawn failure with a new reason,agentCommandNotRunnable, instead of returning the stock CLI. The saved value is kept out of the error text, because a saved command line can contain a secret.launchFolderMissingkind: shared kinds, user copy, refusal details, renderer text and 6 languages. It surfaces on the existing launch card with Retry..exe/.com/.cmd/.bat. Windows can't start an extensionless file, so before this the spawn failed with a generic error that didn't mention the setting. (A test against main's resolver showed it returning an extensionless file.)Mixed versions. No request or stream shape changes. An older client that receives the new failure kind shows the sentence the host wrote, the same fallback it uses for
launchFolderMissing.Why
The common pattern for a configured agent program: when the user explicitly names a program that can't be run, that is an error naming the setting, never a silent swap to the default binary. A silent swap hides the problem: you think you're testing your nightly build or wrapper, but you aren't. Refusing at the one resolver that #25721 already shares between the chat and its model list fixes every surface at once, with no new plumbing.
Differences from the common pattern:
Considered and dropped: looking up a configured Claude name on the login-shell PATH instead of #25721's, and a different
PATH/Pathrule. A test against main found no case where #25721's lookup misses a program the chat would have found.Linked Issue
N/A (maintainer change; follows #25721).
Visual Proof
Live run on Windows in a hidden dev build, with stand-in Claude/Codex programs (
.cmdfiles that print a fixed reply) as the Command. Before (main) isn't pictured: in the refused cases main silently ran the stock CLI found onPATH, which looks like an ordinary chat.<program> app-server; reply renders.--add-dir …)--verbose--profile workTesting
Unit tests:
$variables, missing, relative and non-executable paths, and Windows extensionless files are refused, never replaced by the stock CLI.Checks:
CI: all jobs green except one repository check that fails on main itself (see the PR comment).
Windows (low-spec machine), hidden dev build, stand-in programs, driven over CDP. All 11 cases passed on a480d32. The final head 8e48f0f changes only comments and a test string. The machine's real Claude/Codex config files never changed (hashed after every case). Not run live: macOS/Linux, SSH, real Claude/Codex CLIs, typing the Command in the Settings screen (it was set through the app's settings store).
I manually tested these changes locally
Automated tests added/updated, or explained why not below
Notes
npx claudecan show a model list while the chat refuses..exe.