feat(cli): honor session config overrides + SSH remote when spawning agents#888
Conversation
…agents The CLI (`maestro send`, `maestro run-playbook`) previously spawned agents locally and silently ignored per-agent/per-session configuration. This was called out as a CLAUDE.md violation for SSH, and a silent capability gap for everything else: customArgs, customEnvVars, customModel, and customEffort set in the desktop app were simply dropped at the CLI boundary. This change brings the CLI to parity with the desktop process handler. Changes: - Extend `SessionInfo` with `customArgs`, `customEnvVars`, and `sessionSshRemoteConfig` so the CLI can read everything the desktop persists. - `spawnAgent()` now runs every spawn through `applyAgentConfigOverrides()` (the same merger the desktop uses), layering session → agent-config → defaults just like `process:spawn` does. Covers customArgs (shell-quote aware), customEnvVars (merged into local env and forwarded to SSH), customModel and customEffort (applied via agent `configOptions` argBuilders, so `def.modelArgs()` is no longer called twice). - Loosen `applyAgentConfigOverrides` signature to accept `AgentDefinition` in addition to `AgentConfig`. The function only reads `configOptions`/`defaultEnvVars`; accepting the narrower type lets the CLI pass the static agent definition directly. - SSH remote execution is wrapped via `wrapSpawnWithSsh()`, loaded lazily through `await import()` so the local spawn path never pulls in `execFile`/`which` helpers. - Hard-fail when `sshRemoteConfig.enabled` is true but the remote can't be resolved. Previously the desktop logged a warning and silently fell back to local execution — the CLI now returns a clear error instead, because a user who opted into SSH doesn't want their prompt running on the controller's machine if the remote is misconfigured. - Thread all overrides from `send.ts` and `batch-processor.ts` (both the task run and the synopsis call) so playbook automation respects the same config the desktop uses. Tests: - 25 new tests in `src/__tests__/cli/services/agent-spawner.test.ts` covering: session/agent-level customArgs and customEnvVars precedence, shell-quote parsing, customModel flow via configOptions, SSH wrapper invocation, stdin passthrough for large prompts, hard-fail on unresolvable remote, baseline flags preserved, read-only mode, resume vs fresh session, Codex working-dir/resume args. - Full CLI suite: 718 tests passing. Docs: - `CLAUDE.md` SSH section now documents the CLI-parity requirement and the hard-fail expectation. - `docs/agent-guides/CLI-PLAYBOOKS.md` corrects the "CLI vs Desktop Spawning" section to reflect what is and isn't honored. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR aligns CLI agent spawning with desktop per-session overrides (model, effort, args, env), adds optional SSH remote execution via a dynamically imported SSH wrapper, and changes semantics so an enabled-but-unresolvable SSH remote produces an explicit failure instead of falling back to local execution. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant CLI as CLI Command
participant Spawner as Agent Spawner
participant SSHW as SSH Wrapper (dynamic)
participant Remote as Remote Host
participant Local as Local Agent
User->>CLI: Invoke agent with session overrides + sessionSshRemoteConfig
CLI->>Spawner: spawnAgent(options with customArgs/customEnvVars/sshRemoteConfig)
Spawner->>Spawner: applyAgentConfigOverrides(session > agent > defaults)
Spawner->>Spawner: Merge env (shell <- agent defaults <- customEnvVars)
alt SSH enabled
Spawner->>SSHW: dynamic import & wrapSpawnWithSsh(command, args, remoteConfig)
SSHW->>SSHW: resolve remote
alt remote resolved
SSHW->>Remote: SSH exec wrapped command (write sshStdinScript then end stdin)
Remote-->>SSHW: stdout/close
SSHW-->>Spawner: proxy results
else remote not resolved
SSHW-->>Spawner: unresolved error (no local fallback)
Spawner-->>CLI: sshUnresolvedFailure
end
else SSH disabled
Spawner->>Local: spawn(command + customArgs, env)
Local-->>Spawner: stdout/close
end
Spawner-->>CLI: Agent output or failure
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
Greptile SummaryThis PR brings the CLI spawner (
Confidence Score: 4/5Safe to merge after fixing the env-precedence regression; SSH and config-override logic is otherwise well-structured. One P1 defect: agent defaultEnvVars unconditionally override shell env values for local spawns, contradicting the documented precedence. All other findings are P2 or informational. The hard-fail SSH behavior, config-override precedence, and test coverage are solid. src/cli/services/agent-spawner.ts — the applyEnvLayers call sites at lines ~293-299 and ~969-975 need to pass only user-configured env vars, not the full effectiveCustomEnvVars that bundles agent defaults. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[spawnAgent] --> B{toolType}
B -->|claude-code| C[spawnClaudeAgent]
B -->|usesJsonLineOutput| D[spawnJsonLineAgent]
B -->|other| E[return error]
C --> F[resolveAgentOverrides\napplyAgentConfigOverrides]
D --> F
F --> G[effectiveCustomEnvVars\n= agentDefaults + userVars]
G --> H[applyEnvLayers\nlocal env build]
G --> I{sshRemoteConfig\n.enabled?}
H -->|agentDefaults shell wins| H1[if not env_k : set default]
H -->|batchModeDefaults shell wins| H2[if not env_k : set default]
H -->|effectiveCustomEnvVars UNCONDITIONAL| H3[Object.assign overrides shell]
I -->|yes| J[maybeWrapSpawnWithSsh\n+ buildSshEnvForRemote]
I -->|no| K[local spawn\nenv = applyEnvLayers result]
J --> L{sshRemoteUsed?}
L -->|null| M[sshUnresolvedFailure\nhard error]
L -->|resolved| N[applySshWrapResult\nenv = process.env for ssh client]
N --> O[spawn ssh command\nfinalizeAgentStdin]
K --> O
Reviews (1): Last reviewed commit: "feat(cli): honor session config override..." | Re-trigger Greptile |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/cli/services/agent-spawner.ts`:
- Around line 57-72: resolveAgentOverrides is returning the merged
effectiveCustomEnvVars produced by applyAgentConfigOverrides, but that value
already includes agent.defaultEnvVars so passing it forward causes
applyEnvLayers/Object.assign to treat defaults as explicit user overrides;
change resolveAgentOverrides to return only the explicit sessionCustomEnvVars
(i.e., the true custom overrides provided by the caller) rather than the merged
effectiveCustomEnvVars from applyAgentConfigOverrides so that applyEnvLayers
preserves shell-provided values over agent defaults; locate
resolveAgentOverrides and ensure the returned effectiveCustomEnvVars contains
only the explicit overrides (or return undefined when none) and leave merging of
defaults to the later applyEnvLayers logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: f0c24845-06af-4508-adcb-4662371c8089
📒 Files selected for processing (8)
CLAUDE.mddocs/agent-guides/CLI-PLAYBOOKS.mdsrc/__tests__/cli/services/agent-spawner.test.tssrc/cli/commands/send.tssrc/cli/services/agent-spawner.tssrc/cli/services/batch-processor.tssrc/main/utils/agent-args.tssrc/shared/types.ts
|
Thanks for the thorough work on this, @chr1syy — the CLI/desktop parity gap has been a sore spot and the hard-fail on unresolvable SSH remote is exactly the right call. The test coverage is also great. One issue before merging, flagged by Greptile as P1 and I've verified it against the code: Agent
if (agentDefaults) {
for (const [k, v] of Object.entries(agentDefaults)) {
if (!env[k]) env[k] = v; // shell wins ✅
}
}
// ...
if (userEnvVars) Object.assign(env, userEnvVars); // ← agent defaults ride in here and clobber shell ❌So for a local spawn with no user-configured env vars, any Before this PR, Suggested fix: have Everything else looks solid — nice surgical refactor around the existing code paths. |
applyAgentConfigOverrides() folds agent.defaultEnvVars into its effectiveCustomEnvVars return value. The CLI spawner was forwarding that merged set through applyEnvLayers() as userEnvVars, so the final Object.assign(env, userEnvVars) was unconditionally overwriting shell env with agent defaults — the opposite of the documented "shell wins for defaults" precedence. resolveAgentOverrides() now returns only the truly user-configured env vars (session customEnvVars → agent-level customEnvVars). Defaults continue to be layered separately by applyEnvLayers() (local) and buildSshEnvForRemote() (SSH), where they correctly yield to shell / user values. Also removes the redundant defaultEnvVars double-spread in buildSshEnvForRemote(). Adds three regression tests: - shell env wins over agent defaultEnvVars when no user customEnvVars - agent defaultEnvVars still applied when shell has not set the key - SSH wrapper still receives agent defaultEnvVars when no user customEnvVars (remote has no shell env to fall back on) Resolves PR RunMaestro#888 review comments from greptile-apps (P1 + P2) and coderabbitai (major). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/__tests__/cli/services/agent-spawner.test.ts (1)
1854-1872: Nit:sshWrapResultparameter type omitsprompt.The
Partial<{...}>type on Lines 1855–1862 doesn't declare apromptfield, but the returned object spreadsprompt: undefinedon Line 1868. If a future test wants to overridepromptvia the overrides arg, TS will reject it. Either drop theprompt: undefinedline (since it's alwaysundefinedand callers can't change it) or addprompt?: stringto the override type to match the realwrapSpawnWithSshreturn shape.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/cli/services/agent-spawner.test.ts` around lines 1854 - 1872, The helper sshWrapResult defines an overrides Partial that omits the prompt field but the returned object includes prompt: undefined, preventing tests from overriding prompt; update the overrides type to include prompt?: string (or prompt?: string | undefined) so it matches the actual wrapSpawnWithSsh return shape and callers can override prompt, or alternatively remove the explicit prompt: undefined line from the returned object—apply the change to the sshWrapResult helper where it constructs the mocked wrapSpawnWithSsh result.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/__tests__/cli/services/agent-spawner.test.ts`:
- Around line 1753-1767: The test mutates process.env.MAESTRO_TEST_ENV but
doesn't guarantee restoration on failure; wrap the mutation and assertions in a
try/finally so the original value is restored even if an assertion fails:
capture the previous value, set process.env.MAESTRO_TEST_ENV = 'from-shell', run
spawnAgent('claude-code', ...), driveSpawnToCompletion(p, 0, CLAUDE_OK()),
assert using spawnCall(), and then restore the original value in a finally block
(delete if prev was undefined or set back to prev). Ensure this mirrors the
sibling test's try/finally pattern and references the same helpers (spawnAgent,
driveSpawnToCompletion, spawnCall).
---
Nitpick comments:
In `@src/__tests__/cli/services/agent-spawner.test.ts`:
- Around line 1854-1872: The helper sshWrapResult defines an overrides Partial
that omits the prompt field but the returned object includes prompt: undefined,
preventing tests from overriding prompt; update the overrides type to include
prompt?: string (or prompt?: string | undefined) so it matches the actual
wrapSpawnWithSsh return shape and callers can override prompt, or alternatively
remove the explicit prompt: undefined line from the returned object—apply the
change to the sshWrapResult helper where it constructs the mocked
wrapSpawnWithSsh result.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 2be98080-7d0e-4fa0-b315-95e3f62304aa
📒 Files selected for processing (2)
src/__tests__/cli/services/agent-spawner.test.tssrc/cli/services/agent-spawner.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/cli/services/agent-spawner.ts
If the assertion inside the "applies session customEnvVars to local spawn env (wins over shell env)" test ever throws, the restore block would never run and MAESTRO_TEST_ENV='from-shell' would leak to subsequent tests. Mirrors the try/finally pattern used by the sibling regression tests added in 0193807. Resolves CodeRabbit minor review on PR RunMaestro#888. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Manual Testing Strategy — PR #888All automated coverage is CLI-side with mocked Preflightgit checkout feat/cli-ssh-remote-agents
npm run build
which maestro-cli # or: alias maestro-cli="node $(pwd)/dist/cli/index.js"Config files used below (Linux paths — substitute
Back up your configs first: Shim agent (makes every scenario observable)Drop a scriptable stand-in for any agent so we can inspect the exact argv and env the CLI produced. Claude Code is a good target because it's the default: cat > /tmp/claude-shim.sh <<'SHIM'
#!/usr/bin/env bash
{
echo "=== SHIM ARGS ==="
printf ' %q\n' "$@"
echo "=== SHIM CWD ==="
pwd
echo "=== SHIM ENV (filtered) ==="
env | grep -E '^(TEST_|MAESTRO_|CLAUDE_|ANTHROPIC_|OPENCODE_|CODEX_|NODE_)' | sort
} >&2
# Emit a well-formed stream-json result so the CLI treats it as success
printf '{"type":"result","result":"shim-ok","session_id":"shim-session"}\n'
exit 0
SHIM
chmod +x /tmp/claude-shim.shPoint Claude at the shim in {
"configs": {
"claude-code": { "customPath": "/tmp/claude-shim.sh" }
}
}Every CLI invocation below should print the shim's stderr block — that's our ground-truth for what spawn() actually received. Scenario A — session
|
| Scenario | Pass signal | Fail signal |
|---|---|---|
| A customArgs | shim argv has the flags | flags missing → override dropped |
| B user env wins | shim env = session value | shim env = shell value → not threaded |
| B shell beats default | shim env = shell value | shim env = agent default → regression |
| B default fills blank | shim env = agent default | key missing → defaults dropped |
| C customModel | --model arg present |
arg missing |
| D SSH remote | ssh child visible, remote output | local spawn / no ssh process |
| E SSH hard-fail | clear error, no local spawn | falls back to local → security bug |
| F precedence | session beats agent | agent value leaks through |
There was a problem hiding this comment.
🧹 Nitpick comments (3)
src/__tests__/cli/services/agent-spawner.test.ts (3)
1856-1874:prompt: undefinedisn't in theoverridesPartial type.Minor: the factory signature declares
Partial<{ command; args; cwd; customEnvVars; sshStdinScript; sshRemoteUsed }>but the returned object carriesprompt: undefined(Line 1870) and spreadsoverridesover it. Callers can't overridepromptvia this helper, and no test needs to — so either drop the field or add it to the Partial type so the shape is self-consistent. Purely cosmetic.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/cli/services/agent-spawner.test.ts` around lines 1856 - 1874, The helper sshWrapResult returns an object that includes prompt: undefined but its Partial<> signature does not allow overriding prompt; update the factory so its shape is consistent by either adding prompt?: string | undefined to the Partial type in the sshWrapResult signature or remove the prompt property from the returned object; modify the sshWrapResult function accordingly so callers can override prompt or the field is omitted.
1670-1685:driveSpawnToCompletionsilently swallows a rejectingresultPromise.
Promise.race([waitForSpawnCall(), resultPromise.then(() => {})])attaches a.thenwith no.catch.spawnAgentis designed to always resolve with anAgentResulttoday, so this is fine in practice — but if a future refactor ever lets it reject (e.g., a programmer error thrown synchronously from the new SSH path), this helper would produce an unhandled promise rejection warning and the test would hang onattempts * yieldTickbefore falling through. Cheap guard:♻️ Proposed tweak
- await Promise.race([waitForSpawnCall(), resultPromise.then(() => {})]); + await Promise.race([ + waitForSpawnCall(), + resultPromise.then( + () => {}, + () => {} + ), + ]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/cli/services/agent-spawner.test.ts` around lines 1670 - 1685, The helper driveSpawnToCompletion attaches resultPromise.then(() => {}) inside Promise.race which can cause unhandled rejections if resultPromise rejects; modify the race to swallow rejections safely by replacing resultPromise.then(() => {}) with a rejection-safe handler (e.g., resultPromise.then(() => {}, () => {} ) or resultPromise.catch(() => {})) so waitForSpawnCall() races against a promise that will not produce an unhandled rejection; update references in driveSpawnToCompletion and keep existing behavior with waitForSpawnCall and subsequent mock spawn handling.
2074-2082: Weaker UUID regex than the sibling test at Lines 1409–1414.
/^[0-9a-f-]{36}$/will happily accept e.g. 36 dashes or a v1 UUID. The earlier uniqueness test uses the full v4 pattern (^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$). Worth tightening here for consistency so both tests fail the same way if session-id generation ever regresses off v4.♻️ Proposed tweak
- expect(args[idx + 1]).toMatch(/^[0-9a-f-]{36}$/); + expect(args[idx + 1]).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/cli/services/agent-spawner.test.ts` around lines 2074 - 2082, The test "Claude generates fresh --session-id when no agentSessionId is provided" uses a too-permissive regex; update the assertion that checks args[idx + 1] to use the stricter v4 UUID pattern used by the sibling test (the regex matching 8-4-4-4-12 hex with the '4' and variant nibble constraints) so session-id generation is validated as a v4 UUID; locate this in the test that calls spawnAgent('claude-code', '/p', 'hi') and uses spawnCall(), driveSpawnToCompletion(), and replace the current `/^[0-9a-f-]{36}$/` check with the v4 UUID regex.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/__tests__/cli/services/agent-spawner.test.ts`:
- Around line 1856-1874: The helper sshWrapResult returns an object that
includes prompt: undefined but its Partial<> signature does not allow overriding
prompt; update the factory so its shape is consistent by either adding prompt?:
string | undefined to the Partial type in the sshWrapResult signature or remove
the prompt property from the returned object; modify the sshWrapResult function
accordingly so callers can override prompt or the field is omitted.
- Around line 1670-1685: The helper driveSpawnToCompletion attaches
resultPromise.then(() => {}) inside Promise.race which can cause unhandled
rejections if resultPromise rejects; modify the race to swallow rejections
safely by replacing resultPromise.then(() => {}) with a rejection-safe handler
(e.g., resultPromise.then(() => {}, () => {} ) or resultPromise.catch(() => {}))
so waitForSpawnCall() races against a promise that will not produce an unhandled
rejection; update references in driveSpawnToCompletion and keep existing
behavior with waitForSpawnCall and subsequent mock spawn handling.
- Around line 2074-2082: The test "Claude generates fresh --session-id when no
agentSessionId is provided" uses a too-permissive regex; update the assertion
that checks args[idx + 1] to use the stricter v4 UUID pattern used by the
sibling test (the regex matching 8-4-4-4-12 hex with the '4' and variant nibble
constraints) so session-id generation is validated as a v4 UUID; locate this in
the test that calls spawnAgent('claude-code', '/p', 'hi') and uses spawnCall(),
driveSpawnToCompletion(), and replace the current `/^[0-9a-f-]{36}$/` check with
the v4 UUID regex.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b3e0e64a-b238-4bee-9b2b-ba83099c1ee5
📒 Files selected for processing (1)
src/__tests__/cli/services/agent-spawner.test.ts
Resolves conflict in src/cli/services/agent-spawner.ts. Both branches fixed the same PR RunMaestro#888 review issue (don't fold agent defaultEnvVars into the "user override" slot of the env layer merge) but with slightly different shapes. Adopts upstream's cleaner single-field return (`userCustomEnvVars`) since our branch's parallel `effectiveCustomEnvVars` field had no remaining callers after 8a36a97. Keeps our `applyEnvLayers` fix (merged defaults+ batchMode iteration so batch-mode wins on shared keys) and the precedence-bug docstring on `buildSshEnvForRemote`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
The CLI (
maestro send,maestro run-playbook) previously spawned agents locally and silently ignored per-agent/per-session configuration set in the desktop app —customArgs,customEnvVars,customModel,customEffort, andsessionSshRemoteConfigwere all dropped at the CLI boundary.This was called out as a CLAUDE.md violation for SSH, and a silent capability gap for everything else. This PR brings the CLI to parity with the desktop
process:spawnhandler.applyAgentConfigOverrides()(the same merger the desktop uses), so customArgs, customEnvVars, customModel, and customEffort are all honored with session → agent-config → defaults precedence.await import()so the local spawn path never pulls inexecFile/whichhelpers. Users can now run playbooks against remote-configured agents.sshRemoteConfig.enabledis true but the remote can't be resolved, the CLI returns a clear error instead of silently falling back to local. A user who opts into SSH doesn't want their prompt leaking onto the controller's machine if the config is wrong.01938078c): agentdefaultEnvVarswere being returned as part ofeffectiveCustomEnvVarsand thenObject.assign'd unconditionally overprocess.env, clobbering shell-exported values.resolveAgentOverrides()now returns only the truly user-configured overrides; defaults are layered separately with "shell wins" semantics.Changes
SessionInfoextended withcustomArgs,customEnvVars,sessionSshRemoteConfigso the CLI can read what the desktop persists.spawnAgent()refactored: builds pre-override args, then callsapplyAgentConfigOverrides()to merge configs; removes the now-redundant explicitdef.modelArgs()call.applyAgentConfigOverridessignature loosened to acceptAgentDefinition(narrower type the CLI has on hand — the function only readsconfigOptions/defaultEnvVars).resolveAgentOverrides(),applyEnvLayers(),buildSshEnvForRemote(),sshUnresolvedFailure()helpers inagent-spawner.ts.send.tsandbatch-processor.ts(task run + synopsis) thread all overrides through tospawnAgent.Tests
Docs
CLAUDE.mdSSH section now documents CLI parity and the hard-fail expectation.docs/agent-guides/CLI-PLAYBOOKS.md"CLI vs Desktop Spawning" section corrected to reflect what is and isn't honored.Test plan
Automated
tsc -p tsconfig.cli.json --noEmitcleantsc -p tsconfig.main.json --noEmitclean (pre-existing unrelatedjs-yaml/picomatchwarnings only)eslint src/exit 0prettier --checkclean on all edited filesvitest run src/__tests__/cli— 721/721 passManual verification — 2026-04-24, Linux
End-to-end run against this branch's freshly-built CLI in an isolated sandbox (
XDG_CONFIG_HOMEredirect to a scratch dir — production Maestro config untouched), using a shim "Claude" that logged actual argv/cwd/env per spawn, and a real SSH remote for the remote-execution scenarios. All ten scenarios pass.customArgsthreaded through--foo,has spaces,--bar(shell-quote parsed)customEnvVarswin over shell envTEST_USER_VAR=from-sessionin spawn env, shell hadfrom-shelldefaultEnvVars(regression fix)OPENCODE_CONFIG_CONTENT=shell-set-valuesurvives spawndefaultEnvVarsfill in when shell absentcustomModelapplied--model opusin argvcustomArgs--from-sessionin argv,--from-agentabsentcustomArgsapplied when session absent--from-agentin argvWrapping spawn with SSH remote executionlog, full remotessh ... claude ...dispatched, real response + usage stats returned; local spawn log emptycustomEnvVarsreach SSH remotePR888_REMOTE_CANARY='reached-the-remote' claude ...visible in dispatchedremoteCommandsuccess:falsewith exact expected error; local spawn log empty — prompt "sensitive prompt that must not leak locally" never reached the local machine🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests