Skip to content

fix(native-chat): refuse a Command that can't run instead of silently running the stock CLI - #25667

Merged
brennanb2025 merged 13 commits into
mainfrom
brennanb2025/native-chat-launch-command
Oct 7, 2026
Merged

brennanb2025 merged 13 commits into
mainfrom
brennanb2025/native-chat-launch-command

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 9 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​179 $\color{#cf222e}{\Huge{\mathbf{−}}}$​12 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​167
Prod 19 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​52 $\color{#cf222e}{\Huge{\mathbf{−}}}$​6 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​46

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 --verbose or FOO=1 claude), native chat quietly started the stock claude/codex on 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:

  • Command empty: unchanged (stock CLI).
  • Command is a runnable program path or name (/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.
  • Command can't be run as a program: before, native chat silently ran the stock CLI. After, the chat doesn't start. It shows the message above, and Retry starts it once you fix the setting. Orchestration workers fail to start with the same sentence. The model-list check never runs the stock CLI for a set Command.
  • ⚠️ Users whose Command is a full command line (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.
  • Terminal agents and terminal-backed chats: unchanged.

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.
  • The new failure kind is wired exactly like the existing launchFolderMissing kind: shared kinds, user copy, refusal details, renderer text and 6 languages. It surfaces on the existing launch card with Retry.
  • Windows: a configured file is accepted only if it ends in .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.)
  • Four comments that said native chat ignores the Command are corrected.
  • Not changed: fix(native-chat): honor saved agent Command and Arguments #25721's tokenizer, Arguments handling, PATH lookup and wiring.

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:

  • Intended: Orca's Command field also drives terminal launches, so a command line that works in a terminal is refused in native chat. The message says how to fix it.
  • Intended: the Windows extension rule (above).

Considered and dropped: looking up a configured Claude name on the login-shell PATH instead of #25721's, and a different PATH/Path rule. 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 (.cmd files that print a fixed reply) as the Command. Before (main) isn't pictured: in the refused cases main silently ran the stock CLI found on PATH, which looks like an ordinary chat.

Case What you see
Claude, Command = absolute path to the stand-in Native chat; the stand-in replies (the model-list check ran the same program).
Codex, Command = absolute path to the stand-in Native chat; started as <program> app-server; reply renders.
Claude, Command = quoted path with a space in a folder name Native chat starts.
Claude, Command = a bare name on the launch PATH Native chat starts.
Valid Command + saved Arguments (--add-dir …) Starts; the Arguments are passed as before (#25721 behavior unchanged).
Claude, Command = the stand-in path plus --verbose Refused with the Settings message and Retry. Nothing started, no model-list check.
Claude, Command = a missing path Refused, same message.
Claude, Command = an existing file with no extension (Windows) Refused, same message.
Codex, Command = stand-in path plus --profile work Refused, naming Codex; no Codex process or model-list check started.
Fix the Command, then Retry on the refused chat The same chat starts in place and replies.

Testing

  • Unit tests:

    • The resolver: blank → stock; runnable path or name → that program; command lines, $ variables, missing, relative and non-executable paths, and Windows extensionless files are refused, never replaced by the stock CLI.
    • Both model-list checks refuse without starting anything.
    • The launch card shows the message with Retry.
    • Resume and queued-send refusals.
    • Tests that previously asserted the stock fallback now assert the refusal. A scratch run against main confirmed they fail there.
  • Checks:

    • Typecheck (node, web), oxlint, the changed-code gate and localization checks.
    • Every test file importing a changed module (216 files).
    • 27 cross-version wire tests.
  • 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

  • Security: the saved Command value never appears in error text.
  • SSH/WSL: native chat runs only on the local host, so there's nothing new there. Paired hosts use their own Command.
  • Performance: no new work. It's the same resolver call, which now throws instead of falling back.
  • Follow-ups (not this PR):
    • The native-chat model picker lists models for a set Command by running it as a terminal command line, so npx claude can show a model list while the chat refuses.
    • On Windows, the first PATH folder holding only an extensionless match gets refused, even when a later folder has the .exe.
    • Codex sign-in, Claude Accounts login and a few Codex maintenance helpers still run the stock CLI.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 5, 2026 22:36
@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d34a633e-608b-41ca-abf2-94238cab8502
📥 Commits

Reviewing files that changed from the base of the PR and between 566e479 and a7bb77b.

📒 Files selected for processing (6)
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/fr.json

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.


📝 Walkthrough

Walkthrough

Claude 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 a7bb7

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 Review

Security architecture risk: 🔵 Low · up to 566e4

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
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The newly reachable executable runs on the owning host with provider launch context, including workspace and account-related environment. The directly affected scope is that runtime's local process and data access, not an executable selected through a new remote-session field.

Trust Boundaries and Controls

  • inferred — Validation restricts recognizable forwarded options; it does not sandbox an arbitrary configured executable or prove that a wrapper honors provider permissions and protocol semantics. Selecting a command remains a trusted-code decision.

Resilience and Maintainability Implications

  • observed — If probe preparation fails, catalog reads return unknown unless a cache exists for the active session's exact fingerprint. They do not fall back to an unrelated default-command catalog.

Hardening Proposals

  • proposed — Make clear in command-setting guidance that configured programs may execute during model discovery as well as chat, and that argument validation is not an isolation boundary for untrusted wrappers.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning 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:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly describes the refusal of invalid custom commands instead of silently using the stock CLI. It captures a key change, though the pull request also adds broader custom-command support.
Description check ✅ Passed The description covers the main template sections and gives detailed change rationale, visual proof, testing, and compatibility notes. Some template items, including the Review and Agent skill upstrea…
Full details: Docstring Coverage

Explanation

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.)

  • Fix all pre-merge checks with AI
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d397130a-7f33-4690-ad31-f1049e76b998
📥 Commits

Reviewing files that changed from the base of the PR and between 57fedee and 4e4d990.

📒 Files selected for processing (78)
  • src/main/agent-launch/agent-launch-executor.ts
  • src/main/agent-launch/agent-launch-mode.ts
  • src/main/claude/claude-agent-sdk-contract-pins.test.ts
  • src/main/claude/claude-agent-sdk-process-spawn.test.ts
  • src/main/claude/claude-agent-sdk-process-spawn.ts
  • src/main/claude/claude-model-catalog-probe.test.ts
  • src/main/claude/claude-model-catalog-probe.ts
  • src/main/claude/claude-stream-json-connection.test.ts
  • src/main/claude/claude-stream-json-connection.ts
  • src/main/claude/claude-structured-launch-resolution.test.ts
  • src/main/claude/claude-structured-launch-resolution.ts
  • src/main/claude/claude-structured-session-acquisition-processless.test.ts
  • src/main/claude/claude-structured-session-acquisition.ts
  • src/main/claude/claude-structured-session-adapter.ts
  • src/main/codex-cli/command.test.ts
  • src/main/codex/codex-app-server-session.test.ts
  • src/main/codex/codex-app-server-session.ts
  • src/main/codex/codex-model-catalog-probe.test.ts
  • src/main/codex/codex-model-catalog-probe.ts
  • src/main/codex/codex-structured-acquire-catalog.ts
  • src/main/codex/codex-structured-launch-resolution.test.ts
  • src/main/codex/codex-structured-launch-resolution.ts
  • src/main/codex/codex-structured-session-adapter.ts
  • src/main/codex/codex-structured-session-state.ts
  • src/main/native-chat/agent-model-catalog/agent-model-catalog-fingerprint.ts
  • src/main/native-chat/agent-model-catalog/agent-model-catalog-service.test.ts
  • src/main/native-chat/agent-model-catalog/agent-model-catalog-service.ts
  • src/main/native-chat/agent-model-catalog/agent-model-catalog-store.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-pre-spawn-first-answer.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-start-failure-writer.test.ts
  • src/main/native-chat/structured-agent-command-resolution.test.ts
  • src/main/native-chat/structured-agent-command-resolution.ts
  • src/main/native-chat/structured-agent-session-create-support.test.ts
  • src/main/native-chat/structured-agent-session-create-support.ts
  • src/main/runtime/orca-runtime-get-worktree-ps.ts
  • src/main/runtime/rpc/methods/orchestration-worker-start-mode.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-start-receipt-wording.test.ts
  • src/main/runtime/structured-agent-model-catalog-wiring.ts
  • src/main/runtime/structured-agent-session-runtime.ts
  • src/main/runtime/structured-claude-runtime-adapter.ts
  • src/renderer/src/components/native-chat/NativeChatLaunchRetry.command-failure.test.tsx
  • src/renderer/src/components/native-chat/agent-session-failure-words-text.ts
  • src/renderer/src/components/native-chat/native-chat-session-option-discovery.ts
  • src/renderer/src/components/native-chat/native-chat-session-option-enrichment.test.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/lib/agent-launch-route-input.test.ts
  • src/renderer/src/lib/agent-launch-route-input.ts
  • src/renderer/src/lib/agent-launch-routing.test.ts
  • src/renderer/src/lib/agent-launch-routing.ts
  • src/shared/agent-launch-intent.ts
  • src/shared/agent-session-failure-copy.ts
  • src/shared/agent-session-failure-words.ts
  • src/shared/agent-session-failure.ts
  • src/shared/agent-session-refusal-details.ts
  • src/shared/agent-session-refusal-reason-words.ts
  • src/shared/node-cli-command-resolution.ts
  • src/shared/structured-agent-session-dispatch-rejection.ts
  • src/shared/structured-native-chat-launch-route.test.ts
  • src/shared/structured-native-chat-launch-route.ts
  • src/shared/tui-agent-launch-command-override.test.ts
  • src/shared/tui-agent-launch-command-override.ts
  • tests/e2e/completed-worker-retirement-resume.spec.ts
  • tests/e2e/helpers/golden-stub-agent.ts
  • tests/e2e/orchestration-legacy-worker-missing-terminal-recovery.spec.ts
  • tests/e2e/orchestration-legacy-worker-restart-recovery.spec.ts
  • tests/e2e/orchestration-worker-settlement-release-cli.spec.ts
  • tests/e2e/orchestration-worker-terminal-visibility.spec.ts
  • tests/e2e/orchestration-worker-transcript-providers.spec.ts
  • tests/e2e/settled-worker-tab-survives-restart.spec.ts
  • tests/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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.ts

Repository: 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/shared

Repository: 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/shared

Repository: 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.ts

Repository: 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.ts

Repository: 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.ts

Repository: 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))

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, raising AgentSessionPreSpawnError with customCommandInvalid/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 cwd on the spawn.
  • Routing: requiresTuiLaunchCommand now means only "cwd outside the workspace root"; hasExplicitTuiLaunchCommand is deleted and worker-start receipts reworded.
  • Model catalog: fingerprints include the invocation, live sessions pin theirs, and the renderer sends worktree so the host probes in the workspace.
  • Failure UX: new customCommandInvalid/customCommandConflict copy in six locales, plus e2e specs explicitly disabling structured chat where a stub command is used.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]`).

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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/main into the branch (c127b05 merges 4e4d990 + 9def4b9); the large upstream delta is required context, not authored work.
  • Reconciled the Claude launch path: main's extracted claudeAgentSdkQueryOptions, single cwd variable, and provider-handle leaf encoding now sit beside this PR's custom-command invocation/env, and the invocation still reaches the SDK spawn callback.
  • Reconciled Codex: upstream's provider-record-reader and session-state renames merged without dropping the custom-command prefixArgs or the app-server launch.
  • Reconciled failure vocabularies: main's providerStartFailed and this PR's customCommandInvalid/customCommandConflict coexist 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 on main and is pre-existing.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

… 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.
@brennanb2025

brennanb2025 commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

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 claude --verbose, FOO=1 claude, npx …), native chat, its model list and native orchestration workers silently ran the stock claude/codex on PATH instead. You would be chatting with a different program than the one you picked, with no sign of it.

What changes for users.

  • A set Command that can't be run now refuses the start. The message is "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 Retry on the chat; Retry starts the same chat once the setting is fixed. Orchestration workers fail with the same sentence. The model-list check never runs the stock CLI for a set Command.
  • An empty Command, a runnable program path or name, saved Arguments, terminal launches and terminal-backed chats are all unchanged.
  • ⚠️ Users whose Command is a full command line now see this message in native chat instead of having it silently ignored. Their terminal is unchanged. The fix is to put the program in Command and supported flags in Arguments.

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 agentCommandNotRunnable failure instead of returning the stock CLI. The saved value is kept out of the error text. #25721's tokenizer, Arguments handling and wiring are untouched.

Fixed in review.

  • Windows: a configured extensionless file is refused with the setting message. Before, it failed later with a generic spawn error; a test against main showed main returning such a file.
  • Four comments that said native chat ignores the Command were corrected.
  • A test fixture no longer puts the saved value in the error text.
  • Earlier rounds (the previous design) are recorded in the PR history. What carried over is the failure copy and its six translations.

Considered and dropped: a different Claude PATH lookup and a different PATH/Path rule. Tests against main found no case where #25721's lookup is wrong.

Deferred (not this PR).

  • The native-chat model picker, with a set Command, lists models by running the Command as a terminal command line, so npx claude can show a model list while the chat refuses. That is a separate decision about where the picker gets its list.
  • Windows, P3: if the first PATH folder holding the name has only an extensionless file, the start is refused, even when a later folder has the .exe. Main also failed here, with a vaguer error. Fix it together with the existing case where the empty-Command lookup can pick an extensionless claude.
  • These still run the stock CLI:
    • Codex sign-in
    • Claude Accounts login
    • the Codex hook-trust grant
    • the rate-limit readers
    • Codex session-index repair
    • Codex state-database backfill
  • fix(native-chat): honor saved agent Command and Arguments #25721's Arguments message writes "Settings > Agents"; the rest of the app mostly uses "→".

Verified.

  • CI on 8e48f0f: every job passed except unit shard 1 (and the verify job that aggregates it). That shard has one failure, config/scripts/vitest-sqlite-runtime-boundary.test.ts, which flags src/main/runtime/rpc/methods/agent-launch-instant-tab.test.ts. That file came to main with feat(agent-launch): show an agent's tab at once, where the caller asked #25430 after this PR's base, and the check came with [CI] Bound YAML merge conversion and close SQLite routing review gaps #25998. This PR touches neither file nor the Node-runtime file list, so this is main's own failure, not a new one.
  • Local:
    • typecheck (node, web), oxlint, the changed-code gate and localization checks;
    • every test file importing a changed module (216 files, 2,689 tests);
    • 27 cross-version wire tests.
  • Fresh review of the final diff: no P0, P1 or P2. It confirmed no remaining path where a set Command silently runs the stock CLI: start, resume, queued send, both model-list checks, Claude's version check and orchestration worker start. The worker-start part was read in code; no test covers it.
  • Live Windows run (low-spec machine; hidden dev build; stand-in .cmd programs; driver Codex gpt-5.6-terra on the laptop; screenshots in the PR body): all 11 cases passed on a480d32. That covers the absolute, quoted, bare-name and Arguments cases (which start) and the argument, missing, extensionless and Codex cases (refused, with nothing started), plus Retry after a fix. The final head adds only comments and a test string. The machine's real Claude/Codex config files never changed (hashed after every case).

Not verified.

  • macOS or Linux live runs, SSH or a paired host live, real Claude/Codex CLIs.
  • Typing the Command in the Settings screen (it was set through the app's settings store).
  • Orchestration worker start live.

…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.
@brennanb2025 brennanb2025 changed the title Honor custom Claude and Codex commands in native chat fix(native-chat): refuse a Command that can't run instead of silently running the stock CLI Oct 7, 2026
@brennanb2025
brennanb2025 merged commit 923ef7c into main Oct 7, 2026
30 of 32 checks passed
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