跳到正文
DEV Community· jidonglab·· 46 分鐘前AI 評分70

Claude Code 環境變數不會跨 Bash 呼叫保留,80 次任務中有 3 次回退至 production

Claude Code Env Vars Don't Persist: 3 of 80 Runs Fell Back to Prod

AI 導讀

Claude Code 每次 Bash 工具呼叫都會啟動新 shell,因此一個呼叫中以 `export` 設定的環境變數不會保留;作者在 80 次任務中記錄到 3 次腳本因變數遺失而採用 `production` 預設值。

正文

I told Claude Code export APP_ENV=staging. My next message said to run the script. It printed target = production.

Nothing got deployed. target.py is a one-line stub in a throwaway repo, built around os.environ.get("APP_ENV", "production"), a default you've probably seen in real code. But Claude Code env vars don't persist between Bash tool calls, and I wanted to know how often that actually lands. So I ran 97 headless sessions and counted.

TL;DR

  • Every Bash tool call in Claude Code gets a fresh shell. export, source .venv/bin/activate, functions, aliases and umask all vanish before the next call.
  • cd is the exception, and only when the command succeeds and stays inside the project. In subagents it didn't carry at all.
  • Claude usually works around this by repeating the export. In 80 task runs it missed 3 times, and all 3 ran the script with the fallback value.
  • Each miss was reported honestly, after the script had already run.
  • For a real fix, set state at session start: env in settings.json for plain values, a SessionStart hook writing to $CLAUDE_ENV_FILE for a virtualenv.

Why don't Claude Code env vars persist between Bash calls?

Claude Code env vars don't persist because each Bash tool call starts a new shell, so anything held in shell memory dies with that call. Only the working directory is tracked across calls.

I set eight kinds of state in one call and read them back in the next. All 3 runs gave identical results.

Set in call 1 Seen in call 2
export LAB_EXPORT=1 empty
LAB_PLAIN=2 empty
source .venv/bin/activate VIRTUAL_ENV empty, no python on PATH
lab_fn() { ... } not found
alias lab_alias=... not found
umask 077 back to 022
set -o noclobber off
cd sub kept

You can see it for yourself with one command:

claude -p 'Make two separate Bash calls, one at a time: first "export FOO=1", then "echo FOO=$FOO". Show me the second output verbatim.' --allowedTools Bash

It comes back FOO= with nothing after the equals sign.

The trap is that row at the bottom. cd working makes the tool feel like one long terminal session, so you assume the rest of your shell came along too.

When does cd persist in Claude Code?

cd persists in the main session when the command succeeds and ends inside the project. Break either condition and the shell stays where it was.

Command Next pwd
cd sub proj/sub
cd /tmp (outside the project) back at proj, with the note Shell cwd was reset to ...
cd sub && false (exit 1) still proj
cd sub inside a subagent, then pwd still proj (2 of 2 runs)

The subagent row is the sneaky one. cd sub prints nothing and exits 0, then the next call is back at the root. A relative path written after that lands somewhere you didn't plan. Subagents do start in the main session's current directory, though: after cd sub in the parent, a subagent's first pwd was proj/sub in 2 of 2 runs.

How often does Claude actually trip on this?

Rarely. Across 80 runs of ordinary-sounding tasks on Sonnet 5.5, Claude ended up with the wrong environment 3 times, and all 3 involved a variable with a silent default.

Task (10 runs each) Correct Wrong
Export, then run the script 10 0
Export, list files, read script, run it 10 0
Export, then a subagent runs the script 10 0
Export, 8 unrelated Bash calls, then run 9 1
Export in one message, run in the next 8 2
Activate venv, then run 10 0
Activate venv, then a subagent runs it 10 0
Activate venv in one message, run in the next 10 0

The model knows this rule. It wrote export APP_ENV=staging && python3 target.py, re-exported in each command, and briefed subagents with the variable spelled out in their prompt. That last part surprised me. I expected delegation to be the weak spot, and it scored 20 of 20.

The misses came from two places:

  • Distance. In one run Claude issued export APP_ENV=staging as its own call, did eight other things, then ran a bare python3 target.py.
  • A message boundary. In all 10 of these runs, Claude skipped the bare export and warned that it wouldn't last. My next message ignored the warning and asked for the run. Eight times it prefixed the command with APP_ENV=staging on its own. Twice it ran the bare command.

That second miss is partly on me, and I think it's the realistic one. You skim the reply, type the next instruction, and move on.

All 3 misses were reported in the final reply. "It was not set when target.py ran." That sentence is useful after a read-only script. After terraform apply it's a postmortem.

Ten runs per pattern is too small to quote as a failure rate. The finding I'd trust is the split: 3 misses in 50 variable runs, 0 in 30 venv runs. A missing dependency throws ModuleNotFoundError. A missing variable with a default throws nothing.

How do you make Claude Code env vars persist?

Set them before the session starts, since nothing Claude runs mid-session survives the call. I tested four ways.

Method APP_ENV venv Subagent
env in .claude/settings.json works no not tested
Activate, then launch claude from that shell works works not tested
SessionStart hook writing to $CLAUDE_ENV_FILE works works works
CLAUDE_ENV_FILE=./env.sh claude at launch works works APP_ENV works

Plain values: use env in settings. It's the smallest fix.

{ "env": { "APP_ENV": "staging" } }

It can't run anything, though. With this alone, python3 still resolved to /usr/bin/python3 and check.py died with ModuleNotFoundError.

Virtualenvs and anything that needs a script: use a hook. Whatever it appends to $CLAUDE_ENV_FILE applies to every later Bash call, subagents included.

{
  "hooks": {
    "SessionStart": [{
      "hooks": [{
        "type": "command",
        "command": "printf 'source \"%s/.venv/bin/activate\"\\nexport APP_ENV=staging\\n' \"$CLAUDE_PROJECT_DIR\" >> \"$CLAUDE_ENV_FILE\""
      }]
    }]
  }
}

Use >>, so you don't wipe what another hook wrote. Put per-developer values in .claude/settings.local.json and keep secrets out of anything committed.

Can Claude change the environment during a session?

No, not in anything I tried. The env file behaves as a snapshot taken at session start.

  • With the hook setup, $CLAUDE_ENV_FILE was empty inside the Bash tool, so Claude had nothing to append to.
  • With the launch variable, the path was visible and the file could be edited. I appended export APP_ENV=edited-midsession, confirmed it with cat, and the next call still said staging. Same result in 2 of 2 runs.

If you need to switch from staging to prod, restart the session. For a single command, ask for the inline form: APP_ENV=prod python3 target.py.

Should scripts default to production at all?

No, and this is the cheapest fix in the post. All 3 misses needed a fallback to go wrong quietly.

# silently picks prod when the variable is missing
target = os.environ.get("APP_ENV", "production")

# stops right here with a KeyError
target = os.environ["APP_ENV"]

The second line turns every miss in that table into a crash before any work happens. Default to the safe environment if you must have a default. Anything an agent can run should fail loudly on missing config.

What did this test cover?

Claude Code 2.1.318 on macOS with zsh, headless claude -p with Sonnet 5.5, project settings only, 97 sessions at about $2.08 of list-price usage. I didn't measure interactive sessions, other models or Linux, so run the one-liner above on your own setup first.

Do Claude Code env vars persist between Bash calls?

No. Claude Code env vars set with export in one Bash tool call are gone by the next, along with activated virtualenvs, functions, aliases and umask, because every call starts a fresh shell. Only cd carries over, and only in the main session on a successful command that stays inside the project. Claude works around this most of the time (77 of 80 runs here), but for anything that matters, define the state before launch with env in .claude/settings.json or a SessionStart hook that appends to $CLAUDE_ENV_FILE, and remove production defaults from your scripts.


Written by the developer behind Preterview, an interview prep platform.

來源:DEV Community · dev.to