Skip to content

feat(claude-agent-sdk): honor working_dir via ClaudeAgentOptions.cwd (#348) - #349

Merged
jrob5756 merged 2 commits into
mainfrom
feature/348-claude-agent-sdk-working-dir
Jul 31, 2026
Merged

feat(claude-agent-sdk): honor working_dir via ClaudeAgentOptions.cwd (#348)#349
jrob5756 merged 2 commits into
mainfrom
feature/348-claude-agent-sdk-working-dir

Conversation

@jrob5756

Copy link
Copy Markdown
Collaborator

Closes #348.

What

ClaudeAgentSdkProvider declared working_dir=False, so conductor validate rejected any workflow setting agent.working_dir or runtime.working_dir against it. The declaration was accurate — execute() built ClaudeAgentOptions(...) without a cwd, so the claude CLI and every stdio MCP server it spawned ran in whatever directory the Conductor process happened to be in.

Until #335/#346 the stated reason was that MCP servers were rejected at the factory. That premise is gone, leaving a plain parity gap against copilot (copilot.py:972) and claude (claude.py:1147).

This forwards the engine-resolved directory as ClaudeAgentOptions.cwd and flips the capability.

How

Verified against claude-agent-sdk 0.2.82:

  • ClaudeAgentOptions.cwd (types.py:1880) reaches open_process(cwd=...) and process_env["PWD"] in _internal/transport/subprocess_cli.py.
  • Stdio MCP servers inherit it from the claude subprocess. McpStdioServerConfig (types.py:603) has only type/command/args/envno cwd field — so unlike copilot.py::_mcp_servers_for_cwd there is no per-server stamping to add. _translate_mcp_servers and _write_mcp_config are untouched.
  • add_dirs is a separate axis (--add-dir flags that widen accessible directories, not a cwd). Conductor does not set it, so there is no interaction to handle.

The path is passed verbatim. WorkflowEngine._resolve_agent_working_dir has already rendered, absolutized, normalised, and existence-checked it; re-resolving here would collapse the symlink aliases the engine preserves on purpose.

The two open questions from the issue

Non-existent directory — no extra guard added. The engine raises ExecutionError before any provider call, and a directory that disappears afterwards surfaces as the SDK's CLIConnectionError("Working directory does not exist"), which execute()'s existing except Exception already wraps in ProviderError.

cwd vs add_dirs — orthogonal, as above.

Worth knowing

cwd also determines which CLAUDE.md and local settings the CLI loads, and it is the SDK's session-store project key (_internal/session_resume.py). That is the point of the feature and matches Copilot. The security-relevant part is already covered: strict_mcp_config=True is unconditional, so a .mcp.json in the new directory cannot inject undeclared servers.

Changes

  • providers/claude_agent_sdk.py — forward cwd; flip CAPABILITIES.working_dir to True
  • tests/test_providers/test_claude_agent_sdk.py — new TestWorkingDirectory (5 tests)
  • tests/test_providers/test_capabilities.py — flip the matrix entry
  • docs/providers/experimental.md — carve-out row and sample banner
  • AGENTS.md — parity note
  • CHANGELOG.md — Unreleased entry (the [0.1.23] mention stays; it is historical)

Out of scope: wiring reasoning.effort to the SDK's effort field (a separate declared gap) and exposing add_dirs.

Testing

The new tests use the real ClaudeAgentOptions rather than a Mock, so a renamed or removed SDK field fails here instead of passing silently. They cover the resolved directory reaching cwd, the os.getcwd() fallback, verbatim passthrough (symlink not collapsed), composition with MCP servers, and the descriptor matching the wiring.

  • 3012 passed / 3 skipped across test_providers, test_config, test_engine (the provider skip is the unrelated aca extra)
  • make check and make validate-examples both clean
  • Before/after check: a workflow with working_dir on this provider previously failed with ConfigurationError: ... capabilities.working_dir=False and now validates

Note the SDK tests are importorskip-gated; CI's test job installs the extra (ci.yml:109), the typecheck job does not.

Jason Robert and others added 2 commits July 31, 2026 09:00
…348)

The provider declared `working_dir=False`, so `conductor validate` rejected
any workflow setting `agent.working_dir` or `runtime.working_dir` against it.
The declaration was accurate but left the provider out of step with `copilot`
and `claude`, both of which honor the engine-resolved directory.

`execute()` now forwards the resolved directory as `ClaudeAgentOptions.cwd`,
which the SDK applies as the `claude` subprocess's cwd. Stdio MCP servers pick
it up by inheriting from that subprocess, so there is no per-server stamping as
in `copilot.py::_mcp_servers_for_cwd` — the SDK's `McpStdioServerConfig` has no
cwd field, leaving `_translate_mcp_servers` untouched.

The path is passed verbatim: `WorkflowEngine._resolve_agent_working_dir` has
already rendered, absolutized, normalised, and existence-checked it, and
re-resolving would collapse symlink aliases the engine preserves on purpose.
No provider-side `is_dir()` guard either — a directory that disappears after
that check surfaces as the SDK's own `CLIConnectionError`, which the existing
`except Exception` already wraps in `ProviderError`.

Note that cwd also selects which `CLAUDE.md` and local settings the CLI loads,
and is the SDK's session-store project key. The unconditional
`strict_mcp_config=True` still prevents a `.mcp.json` in that directory from
injecting undeclared servers.

Tests use the real `ClaudeAgentOptions` rather than a Mock so a renamed or
removed SDK field fails loudly instead of passing silently.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Review follow-ups on the working_dir wiring.

`os.getcwd()` was evaluated outside `execute()`'s try, so a deleted process
cwd escaped as a bare `FileNotFoundError` with no path and no suggestion,
breaking the method's `ProviderError` contract. It now resolves through a
dedicated handler that names the agent, the subsystem, and two remedies,
rather than falling through to the generic "check the CLI is installed" arm.

The SDK reuses `CLIConnectionError` for failures to *spawn* the CLI, so a
missing working directory, a path that is a file (ENOTDIR), and an unreadable
one (EACCES) were all reported as connection problems -- "check the binary is
executable and that no firewall is blocking" -- and all marked retryable even
though none can succeed on a second attempt. `_classify_startup_failure` now
distinguishes them, shared by both classifiers so detection lives in one
place. Genuine connection drops keep the old advice and stay retryable. The
ENOTDIR/EACCES hint names both possible causes because the errno text does
not say whether the offending path is the working directory or the binary.

Skipping the provider-side `is_dir()` guard is only defensible if the wrapped
error is actionable, so this is a prerequisite of that decision rather than a
separate improvement.

Also pins the capability flip where it is user-visible: nothing asserted that
`conductor validate` now accepts these workflows. The real-descriptor
cross-check covers both the per-agent and workflow-level validator branches,
which are separate code paths.

Documents that the CLI loads `CLAUDE.md` and `.claude/settings*.json` from
its working directory, so pointing an agent at an untrusted checkout runs
that checkout's instructions -- `strict_mcp_config` covers MCP servers but
not hooks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jrob5756
jrob5756 force-pushed the feature/348-claude-agent-sdk-working-dir branch from ffd038e to 296e506 Compare July 31, 2026 13:04
@jrob5756
jrob5756 marked this pull request as ready for review July 31, 2026 13:08
@jrob5756
jrob5756 merged commit f39bd63 into main Jul 31, 2026
10 checks passed
@jrob5756
jrob5756 deleted the feature/348-claude-agent-sdk-working-dir branch July 31, 2026 13:29
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.

claude-agent-sdk: forward the resolved working_dir to ClaudeAgentOptions.cwd

1 participant