If you've started handing Claude Code anything more than the simplest tasks, you've probably run into this: you call cd some-dir in one Bash invocation, then run ls in the next, and the output comes back from your home directory instead. The same thing happens with export FOO=bar — the next call sees an empty $FOO.
I burned the better part of a morning on this when I rebuilt the automated content pipeline for Dolice Labs, the four-site Claude/Gemini/Antigravity/Rork blog network I run as an indie developer. Once you understand the reason, the fix is one line. But until you do, it really looks like Claude Code is broken.
Symptom — Your cd is Silently Discarded
A short reproduction:
# Bash call #1
cd /tmp/repos/claudelab.net
# Bash call #2
ls
# → returns the home directory listing, not the repoexport behaves the same way:
# Call #1
export GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxx
# Call #2
echo "$GITHUB_TOKEN"
# → emptyWhat makes this confusing is that if you paste both calls into your own terminal, it works exactly as expected. That's why so many people start assuming the tool is buggy. It isn't — the behavior is intentional.
Cause — Each Bash Call Spawns a Brand-New Shell
Claude Code (and the mcp__workspace__bash tool in Cowork mode) starts a new subprocess for every Bash invocation. The moment the call returns, the process is destroyed along with any state mutations: cwd, exported variables, shell options, all of it.
The Cowork bash tool description spells this out: "Each call is independent — no cwd/env carryover between calls. Use absolute paths." Claude Code's main Bash tool follows the same model. Mentally, it's closest to "a remote shell that does not share history between commands."
Once you internalize that, the fix is simple: stop trying to carry state across calls. Below are the five patterns I rely on day to day.
Fix 1 — Chain Everything with && in a Single Call
This is the workhorse. Whenever a cd is followed by an action, both belong in the same Bash invocation, joined with &&.
# Bad — the cd is lost
cd /tmp/repos/claudelab.net
git pull --rebase origin main
# Good — one call, one shell
cd /tmp/repos/claudelab.net && git pull --rebase origin mainIt's a one-line trick, but in my experience it eliminates roughly 70% of the related failures by itself. Pinning this rule in CLAUDE.md (something like "when running multiple shell steps, combine them into a single Bash call with &&") cuts down on auto-generation accidents as well.
Fix 2 — Use Absolute Paths Aggressively
The other half of the answer is "don't use cd if you don't have to." Across my Dolice Labs scheduled tasks, I deliberately avoid cd and operate on absolute paths.
# Bad — depends on an implicit cwd
cd content/articles/ja
ls
# Good — absolute
ls /tmp/repos/claudelab.net/content/articles/jagit -C <dir> <subcmd> follows the same spirit and lets you pin the working directory without ever moving:
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 mainThis survives being split across multiple Bash calls without any extra ceremony.
Fix 3 — Pass Environment Variables Inline
export doesn't survive between calls either. For values used in a single command, set them inline:
# Bad
export GITHUB_TOKEN=ghp_xxxxxxxx
git clone https://${GITHUB_TOKEN}@github.com/foo/bar.git
# Good — variable scoped to the single command line
GITHUB_TOKEN=ghp_xxxxxxxx git clone https://${GITHUB_TOKEN}@github.com/foo/bar.gitHardcoding a token per call is obviously a bad idea, so in my pipelines I read it from a file on the workspace each time I need it:
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.netVariables that you reference repeatedly, like $WS, are simpler to recompute at the top of each Bash call (WS=$(ls -d /sessions/*/mnt/Dolice\ Labs | head -1)) than to try to share.
Fix 4 — Persist Long-Lived Values in CLAUDE.md or a Tempfile
For values you'd otherwise want to set once and forget — repo paths, the location of your tokens — put the convention in CLAUDE.md ("repos live under /tmp/repos/<site>", "PATs are read from _documents/_github_tokens/"). Claude Code will pick up those rules and reconstruct them in each Bash call.
If you really need to round-trip a value between two calls, write it to a tempfile:
# Call #1: stash state on disk
echo "/tmp/repos/claudelab.net" > /tmp/cc_workdir
echo "ghp_xxxxxxxx" > /tmp/cc_token
# Call #2: read it back
WORK=$(cat /tmp/cc_workdir)
TOKEN=$(cat /tmp/cc_token)
git -C "$WORK" push "https://${TOKEN}@github.com/foo/bar.git" mainFor anything sensitive, delete the file right after use (rm /tmp/cc_token).
Fix 5 — Pull Complex Pipelines Into a Shell Script
Once a workflow involves three or more cd/export moves, stop fighting one-liners. Write the whole thing to a script file and invoke it:
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
# the actual long-running steps
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
# A separate Bash call runs the script
bash /tmp/deploy.shset -euo pipefail makes the script fail loudly instead of silently skipping past a broken step, which saves a lot of debugging time. There's also a side benefit: you can reproduce the exact same script in your own terminal.
The Same Constraint Applies to Related Tools
The "discard state on return" model isn't unique to the Bash tool. Claude Code's PowerShell tool starts a fresh session per call, so Set-Location doesn't persist either. Cowork's mcp__workspace__bash works the same way. Sub-agents (the Task tool) also don't inherit variables or cwd from the caller.
The Claude Code ecosystem as a whole is built around stateless execution units. Aligning your own scripts with that assumption — rather than fighting it — turns out to be the durable path.
Design Rules I Settled On
When I cleaned up the four Dolice Labs pipelines, I converged on three rules and have stuck with them since:
- Every Bash call should be safely re-runnable on its own. Makes partial reruns and debugging much easier.
- State travels via files or path arguments, not variables. A tempfile under
/tmp, or a fully qualified path passed in. - Anywhere you'd write
cd, replace it with&&chaining orgit -C. Over the long run this has been the lowest-incident policy.
Across the Cowork-scheduled jobs that push 4 articles per day per site (16 articles daily across the network), incidents in the "the cd didn't take effect" category have essentially fallen to zero since the switch. I've been online since 1997 and writing software professionally for years, but the "discard state per call" model still felt unusual at first. Getting fluent with it is, I'd argue, part of the shell literacy required for the agentic era.
Hope this saves someone the half-day I lost to it. Next time you hand Claude Code a multi-step Bash plan, the first question to ask is simply: "can this be one line joined with &&?"