Claude Code に少し複雑なタスクを任せ始めると、ほとんどの方が一度はぶつかる症状があります。「cd some-dir を実行して、次の ls を叩いたら、なぜか元の作業ディレクトリで実行されている」という現象です。export FOO=bar も同じで、次のコマンドでは $FOO が空になっていることがあります。
私自身、Dolice Labs で 4 サイトの記事自動投稿パイプラインを Claude Code 主体で組み直していた 2026 年初頭、同じ落とし穴で半日近く溶かしました。原因が分かれば一行で済む話ですが、知らないと「Claude Code が壊れているのではないか」と疑い始めるので、最初に整理しておく価値はあると感じています。
症状 — cd した直後の ls が想定外の場所で動く
具体的には、こういう挙動です。
# 1 回目の Bash ツール呼び出し
cd /tmp/repos/claudelab.net
# 2 回目の Bash ツール呼び出し
ls
# → なぜかホームディレクトリの中身が返ってくるexport も同じ症状になります。
# 1 回目
export GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxx
# 2 回目
echo "$GITHUB_TOKEN"
# → 空文字スクリプトファイルにして手元のターミナルで流すと普通に通るので、最初は「Claude Code 側のバグかも」と疑いがちですが、これは仕様通りの挙動です。
原因 — Bash ツールは 1 回ごとに独立したシェルで起動する
Claude Code(および Cowork mode の mcp__workspace__bash も同じです)は、Bash ツールの呼び出しごとに新しいサブプロセスとしてシェルを立ち上げます。つまり、1 回目の呼び出しが終わった瞬間にプロセスが破棄され、cd や export で変えた状態はすべて失われます。
私の手元では、Cowork 経由の bash ツールの説明にも「Each call is independent — no cwd/env carryover between calls. Use absolute paths.」と明記されており、Claude Code 本体の Bash ツールでも基本ポリシーは同じです。要するに、Claude Code の Bash は「実行履歴を共有しないリモートシェル」だと考えるのが一番近い感覚です。
ここを理解すると、対処方針はシンプルになります。「状態を次の呼び出しに持ち越そうとしない」ことを徹底すればよい、ということです。
対処 1 — && で 1 行のコマンドに束ねる
最も多用するパターンです。cd してから何かを実行する場合は、必ず同じ Bash 呼び出しの中で && で連結します。
# NG: 2 回に分けると cd が消える
cd /tmp/repos/claudelab.net
git pull --rebase origin main
# OK: 1 回にまとめる
cd /tmp/repos/claudelab.net && git pull --rebase origin mainたった一行のテクニックですが、これだけで全体の事故率が体感で 7 割以上減ります。Claude Code に「複数行のコマンドを書くときは 1 つの bash 呼び出しにまとめてください」と CLAUDE.md に書いておくと、自動生成時の事故も減らせます。
対処 2 — 絶対パスを徹底する
「cd を使わないで済むなら、最初から使わない」というのも有効な戦略です。私の場合、Dolice Labs のスケジュールタスクでは cd をほぼ使わず、すべて絶対パスで参照する方針に統一しました。
# NG: 暗黙の cwd に依存
cd content/articles/ja
ls
# OK: 絶対パス
ls /tmp/repos/claudelab.net/content/articles/jagit -C <dir> <subcmd> も同じ思想で、cd なしで作業ディレクトリを指定できる便利な書き方です。
git -C /tmp/repos/claudelab.net pull --rebase origin main
git -C /tmp/repos/claudelab.net add content/
git -C /tmp/repos/claudelab.net commit -m "Add new article"
git -C /tmp/repos/claudelab.net push origin main複数の bash 呼び出しに分かれても安全に動作します。
対処 3 — 環境変数はインラインで渡す
export で永続化したつもりの環境変数も、次の呼び出しでは消えます。一度きりしか使わない変数は、コマンドの直前にインラインで指定するのが安全です。
# NG
export GITHUB_TOKEN=ghp_xxxxxxxx
git clone https://${GITHUB_TOKEN}@github.com/foo/bar.git
# OK: 同じコマンドラインで完結させる
GITHUB_TOKEN=ghp_xxxxxxxx git clone https://${GITHUB_TOKEN}@github.com/foo/bar.gitただし、トークン文字列を毎回ハードコードするとセキュリティ上望ましくないので、私のスケジュールタスクでは「ワークスペース内のファイルから読み出してインライン変数として使う」やり方で統一しています。
GITHUB_TOKEN=$(grep -A1 "^Claude Lab" "${WS}/_documents/_github_tokens/github_tokens.txt" | tail -1 | tr -d '[:space:]')
git clone --depth 1 "https://${GITHUB_TOKEN}@github.com/masakihirokawa/claudelab.net.git" /tmp/repos/claudelab.net$WS のような複数回参照する変数も、必要な bash 呼び出しごとに WS=$(ls -d /sessions/*/mnt/Dolice\ Labs | head -1) を再評価する設計にしておくと混乱しません。
対処 4 — 永続的に持ち回りたい値は CLAUDE.md か一時ファイルに置く
毎回 cd や export を書くのが面倒な値(たとえばリポジトリのパスや GitHub PAT の場所)は、CLAUDE.md に「リポジトリは /tmp/repos/<site> を使う」「PAT は _documents/_github_tokens/ から読む」のように書いておくと、Claude Code が次回からそれを前提に bash を組み立ててくれます。
ファイル経由で値を渡したい場合は、/tmp の下にメモを書き出すのが現実的です。
# 1 回目: 状態を一時ファイルに保存
echo "/tmp/repos/claudelab.net" > /tmp/cc_workdir
echo "ghp_xxxxxxxx" > /tmp/cc_token
# 2 回目: 一時ファイルから読み戻す
WORK=$(cat /tmp/cc_workdir)
TOKEN=$(cat /tmp/cc_token)
git -C "$WORK" push "https://${TOKEN}@github.com/foo/bar.git" mainセキュリティ上、トークンを /tmp に書く場合は実行直後に削除してください(rm /tmp/cc_token)。
対処 5 — 複雑な処理はシェルスクリプトを書き出して実行する
cd と export が 3 つ以上絡むような処理は、無理に 1 行に詰め込まず、シェルスクリプトとしてファイルに書き出してしまうのが結果的に楽です。
cat << 'EOF' > /tmp/deploy.sh
#!/bin/bash
set -euo pipefail
WORK=/tmp/repos/claudelab.net
TOKEN=$(cat /tmp/cc_token)
cd "$WORK"
git pull --rebase origin main
# 何らかの長い処理
git add content/
git commit -m "Daily content update"
git push "https://${TOKEN}@github.com/masakihirokawa/claudelab.net.git" main
EOF
chmod +x /tmp/deploy.sh
# 別の bash 呼び出しから実行
bash /tmp/deploy.shset -euo pipefail を入れておくと、途中でコケた場合に黙って先に進むことがないので、デバッグも楽になります。スクリプトファイルにしておくと、人間が手元のターミナルで同じものを再現できるという副次的なメリットもあります。
同じ仕組みのツール — PowerShell ツール・サブエージェント
ここまで Bash ツールを例に書いてきましたが、同じ「呼び出しごとにステートを破棄する」仕様は、Claude Code の関連ツールにも共通しています。
PowerShell ツールも 1 回ごとに独立したセッションです。Set-Location で移動しても次の呼び出しでは元に戻ります。Cowork mode の mcp__workspace__bash も同じです。サブエージェント(Task ツール)も、呼び出し元のコンテキストや変数は引き継ぎません。
つまり、Claude Code のエコシステム全体が「ステートレスな実行ユニット」を前提に組まれていると考えてよく、これに合わせて自分のスクリプト設計を整えていくのが正解です。
設計のコツ — 「ステートレス前提」で書く
私が Dolice Labs の 4 サイト分の自動投稿パイプラインを整理し直したとき(2014 年から個人開発を続けてきた経験から言うと)、最終的に落ち着いたのは次の 3 原則です。
- どの bash 呼び出しも、それ単体で再実行できる形にする。途中だけ流して動作確認するときに楽です。
- 状態は変数ではなくファイルかパス引数で渡す。
/tmpのメモか、絶対パスで完結させます。 cdを書く場面は&&かgit -Cのどちらかに置き換える。長期で見るとこれが一番事故が少ない方針でした。
Cowork mode のスケジュールタスクで毎日 4 本 × 4 サイト分の記事を自動投稿していますが、上の 3 原則に切り替えてから「cd が効かない系」のエラーは記録上ほぼゼロになりました。1997 年からインターネットとプログラミングに触れ続けてきた身としても、「呼び出しごとに状態を破棄する」というモデルは新鮮で、これに慣れること自体が AI エージェント時代のシェル運用のリテラシーだと感じています。
同じ症状で詰まっている方の参考になれば幸いです。次に Claude Code に複数ステップの bash を任せるときは、まず「これは && で 1 行にできないか?」を最初に自問する癖をつけてみてください。