Claude Media
ClaudeCodeOptionsのcwdが反映されない理由と回避策 — Python Agent SDK

ClaudeCodeOptionsのcwdが反映されない理由と回避策 — Python Agent SDK

claude-agent-sdk-pythonのissueが報告し続けるcwd無視の症状を、報告の中身と公式ドキュメントの仕様から切り分け、回避策と検証方法を示します。

Python版Agent SDKでcwdを指定しても、Claudeが別のディレクトリにファイルを書くことがあります。claude-agent-sdk-pythonのissueには2025年6月から同じ症状の報告が続き、issueはopenのままです。コメントはすべて利用者によるもので、Anthropicの担当者の返信はありません。

報告をたどると、system_promptにclaude_codeプリセットを指定して直った例、使うモデルを変えて直った例、macOSのパス表記を誤解していた例が混在しています。症状は1つでも、原因は1つとは限りません。この記事では、症状ごとの切り分け順と、書き込み先を実際に拘束する方法を示します。

報告された症状の移り変わり

issueのタイトルは cwd in ClaudeCodeOptions does not seem to be honored at all です。最初の報告者はcwdに絶対パスを渡し、bypassPermissionsまで指定したのに、無関係なパスが使われたと書いています。コードは次のとおりでした。

options = ClaudeCodeOptions(
    cwd=Path("/path/to/app"),
    system_prompt="You are a helpful assistant",
    allowed_tools=[...],
    permission_mode="bypassPermissions",
    continue_conversation=True,
    max_turns=10,
)

以降のコメントを時系列に並べると、同じ見出しの下に別々の話が混ざっていることが分かります。

あゆみ

issueのコメントの流れ

  1. 2025年6月15日最初の報告

    cwdが無視され、ランダムなパスが使われる。本文には「opusに切り替えたら問題なくなった」という追記があります。

  2. 2025年6月25日パスの先頭に/privateが付く

    /path/to/appが/private/path/to/appになるという報告。翌日、本人が「Macのシンボリックリンクの挙動で、自分の問題は別だった」と訂正しています。

  3. 2025年7月23日プロンプトで指定すれば動く

    「プロンプトにディレクトリを書く必要があった」という報告です。同じ内容をgemini-2.5-proで試すと問題なく動いたとも書かれています。

  4. 2025年8月〜2026年3月書き込み先のずれと再現報告

    意図しないディレクトリでの作業(権限エラーで止まった)、/tmpへの書き込み、Windowsでの再現、gitのworktreeでメインリポジトリ側のファイルが更新された例が続きます。

  5. 2026年4月5日プリセット指定で解消した報告

    system_promptを指定していなかった環境で、claude_codeプリセットを指定したら直ったという報告。issueの最後のコメントです。

最初の報告者の追記と、7月の報告が示すとおり、同じコードでもモデルを変えると結果が変わったという声が2件あります。cwdの渡し方だけで説明がつく症状ではない、という手がかりです。

cwdの定義とClaudeAgentOptionsへの改名

cwdはSDKの設定オブジェクトで、Claude Codeを起動するサブプロセスの作業ディレクトリを指定するフィールドです。現行のリファレンスではClaudeAgentOptionsの1フィールドとしてcwd: str | Path | None = Noneと定義され、説明は「Current working directory」の一文だけです。

issueタイトルのClaudeCodeOptionsは旧名です。移行ガイドによると、Claude Agent SDK v0.1.0でパッケージ名がclaude-code-sdkからclaude-agent-sdkに変わり、Pythonの型名もClaudeAgentOptionsに改名されました。同じv0.1.0で、system_promptを指定しなかったときの既定動作も変わっています。

v0.0.x(claude-code-sdk)v0.1.0以降(claude-agent-sdk)
Pythonのクラス名v0.0.x(claude-code-sdk)ClaudeCodeOptionsv0.1.0以降(claude-agent-sdk)ClaudeAgentOptions
system_prompt未指定時の既定v0.0.x(claude-code-sdk)Claude Codeのシステムプロンプトv0.1.0以降(claude-agent-sdk)最小限のシステムプロンプト

移行ガイドはこれらの破壊的変更を、分離と明示的な設定を高めるための変更と位置づけています。型名は名前だけの変更ですが、既定のシステムプロンプトの変更は挙動に効きます。issueの最初の報告は旧名の時代のコードですが、system_promptに文字列を渡しているので、どちらの既定でもプリセットは使われていません。改名の経緯はAgent SDK移行ガイドにまとめています。

system_promptの指定で何が変わるのか

system_promptを未指定にすると、ツール呼び出しに必要な最小限のプロンプトが使われ、claude_codeプリセットのほかの内容は含まれません。独自の文字列を渡した場合は、SDKは渡した内容だけを送ります。プリセットは、Claude Code CLIが使うシステムプロンプトそのものです。

くらべる

system_promptの3つの指定

最小の既定

未指定

ツール呼び出しの指示だけ。セキュリティ・安全性の指示などプリセットの残りは入りません。

自分で全部書く

独自の文字列

SDKは渡した内容だけを送ります。ツールの使い方や安全面の指示も自分で書きます。

CLIと同じ

claude_codeプリセット

Claude Code CLIのシステムプロンプト。appendで末尾に独自の指示を足せます。

作業ディレクトリの情報がどこから渡るかは、公式の記述が揃っていません。モデル向けプロンプトのカスタマイズのページには、「作業ディレクトリ、プラットフォーム、シェル、OSのバージョンといった環境情報は、システムプロンプトではなく会話の中で渡される」という説明があります。一方、v2.1.289のclaude --helpは、次のように書いています。

claude --help | grep -A4 exclude-dynamic
  --exclude-dynamic-system-prompt-sections
      Move per-machine sections (cwd, env info, memory paths, git status) from
      the system prompt into the first user message. Improves cross-user
      prompt-cache reuse. Only applies with the default system prompt (ignored
      with --system-prompt). (default: false)

CLIの説明では、cwdは既定のシステムプロンプトの中の項目です。しかも独自の--system-promptを渡すとこのオプションは無視されます。引用した2つの説明からは、「独自の文字列を渡すとcwdの情報が失われる」とまでは読み取れません。確かなのは、独自の文字列やプリセット未指定では、プリセットの内容が入らないことです。

最初の報告者のコードは、system_prompt="You are a helpful assistant"の文字列指定でした。2026年4月の報告者は、プロンプトを何も指定しない状態からclaude_codeプリセットを足して直したと書いています。どちらの場合も、プリセットを使っていない構成です。

回避策 — claude_codeプリセットを指定する

最初に試す価値があるのは、独自の文字列や未指定をやめて、claude_codeプリセットにそろえることです。

from claude_agent_sdk import query, ClaudeAgentOptions
 
options = ClaudeAgentOptions(
    cwd="/path/to/app",
    system_prompt={"type": "preset", "preset": "claude_code"},
    permission_mode="bypassPermissions",
)

独自の指示を足したいときはappendを使います。プリセットの内容を残したまま、末尾に追記できます。7月の報告者が「プロンプトに書く必要があった」と言っているとおり、作業場所を指示文に書いておく方法もあります。

options = ClaudeAgentOptions(
    cwd="/path/to/app",
    system_prompt={
        "type": "preset",
        "preset": "claude_code",
        "append": "Work only under /path/to/app. Do not write files elsewhere.",
    },
)

プリセットとappend・独自プロンプトの使い分けは、Agent SDKのシステムプロンプトをカスタマイズする4つのアプローチで比べています。大量のセッションでキャッシュのヒット率が気になる場合は、同記事のexclude_dynamic_sectionsの節も参照してください。

cwdが効いているかをpwdで確かめる

目視で「直った」と判断するのは危ういので、Claude自身に作業ディレクトリを言わせます。allowed_toolsにBashを含め、最初のプロンプトでpwdの結果を返させます。

async for message in query(
    prompt="Run `pwd` and report the exact output, then stop.",
    options=ClaudeAgentOptions(
        cwd="/path/to/app",
        system_prompt={"type": "preset", "preset": "claude_code"},
        allowed_tools=["Bash"],
        permission_mode="bypassPermissions",
    ),
):
    print(message)

pwdの出力が指定したcwdと一致すれば、サブプロセスは正しい場所で起動しています。macOSで/tmpを指定した場合は、出力が/private/tmpになってもcwdの設定ミスではありません。issueでも、報告者が同じ見え方をシンボリックリンクの挙動だと訂正しています。

ただしpwdの確認は、起動した場所しか見ません。Claudeが絶対パスで別のディレクトリに書き込む症状は、この方法では検出できません。

症状から原因を切り分ける

報告を症状別に分けると、手を付ける順番が変わります。

手順

切り分けの順番

  1. 1

    パスの見え方を疑う

    /tmp/appが/private/tmp/appに見えるだけなら、macOSのシンボリックリンクです。cwdの問題ではありません。

  2. 2

    system_promptの構成を揃える

    独自の文字列や未指定のままなら、claude_codeプリセットに切り替えて再現するか見ます。

  3. 3

    モデルを変えて比べる

    最初の報告者はopusで、別の報告者はgemini-2.5-proで問題が出なかったと書いています。同じコードでモデルだけを替えて挙動を見ます。

  4. 4

    書き込み先をフックで止める

    原因が絞れなくても、cwdの外への書き込みは止められます。次の節のフックを入れます。

Windowsでの再現とgitのworktreeの報告には、issueに原因の説明がありません。worktreeの例では、worktreeで実行したのにメインリポジトリのファイルが更新されています。

書き込み先をcwd配下に固定する

意図しないディレクトリへの書き込みは、権限エラーで止まった報告者がいる一方で、止まらなければそのまま書かれてしまいます。

ここで気を付けたいのがcan_use_toolコールバックです。Pythonのリファレンスによると、can_use_toolは権限評価が確認の段階まで進んだときにだけ呼ばれます。allowed_tools、許可ルール、bypassPermissionsなどの権限モードで承認された呼び出しは、コールバックを通りません。最初の報告のようにbypassPermissionsを使うと、can_use_toolでは書き込み先を検査できません。

すべての呼び出しを検査したいときは、PreToolUseフックを使います。公式は、can_use_toolが通らない呼び出しにも適用したい場合の手段としてフックを挙げています。フックは権限評価のほかの段階より先に実行され、permissionDecision: "deny"で呼び出しを止められます。

from pathlib import Path
from claude_agent_sdk import ClaudeAgentOptions, HookMatcher
 
BASE_DIR = Path("/path/to/app").resolve()
 
 
async def restrict_to_cwd(input_data, tool_use_id, context):
    raw = input_data["tool_input"].get("file_path", "")
    target = (BASE_DIR / raw).resolve()
    if not target.is_relative_to(BASE_DIR):
        return {
            "hookSpecificOutput": {
                "hookEventName": input_data["hook_event_name"],
                "permissionDecision": "deny",
                "permissionDecisionReason": f"{target} is outside {BASE_DIR}",
            }
        }
    return {}
 
 
options = ClaudeAgentOptions(
    cwd=BASE_DIR,
    permission_mode="bypassPermissions",
    hooks={
        "PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[restrict_to_cwd])]
    },
)

BASE_DIR / rawは、rawが相対パスならcwd基準で、絶対パスならそのパスで解決します。resolve()でシンボリックリンクも展開されるので、/tmpのようなリンク経由の書き込みもBASE_DIRの外として弾けます。denyの理由はモデルに返るので、Claudeは同じ場所への再試行を避けやすくなります。

このフックが見るのはファイルを書くWriteとEditのツールだけです。Bashコマンドはfile_pathを持たないため、リダイレクトでの書き込みは素通りします。コマンド実行側は、sandbox設定やdisallowed_toolsのルールで絞ります。サンドボックスの設定はAgent SDKのsandbox設定、フックの書き方はAgent SDKのhooksに詳しくあります。CLIが起動できない、プロセスが途中で終了するといった別種のエラーはAgent SDKのエラー集を参照してください。

まとめ

cwdはClaude Codeのサブプロセスを起動する場所を決めるオプションで、「Claudeがどこに書くか」を保証するものではありません。作業が別の場所に出るときは、claude_codeプリセットとモデルの2点を切り替えて再現を確かめ、書き込み先はPreToolUseフックで止めます。Claude CodeのCLIで同じ作業を試して挙動が違うなら、システムプロンプトの構成の差を疑う手がかりになります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. github.com/anthropics/claude-agent-sdk-python/issues/10
  2. code.claude.com/docs/en/agent-sdk/python
  3. code.claude.com/docs/en/agent-sdk/modifying-system-prompts
  4. code.claude.com/docs/en/agent-sdk/migration-guide
  5. code.claude.com/docs/en/agent-sdk/hooks
  6. code.claude.com/docs/en/agent-sdk/user-input
この記事を共有:XはてブLinkedIn