◉CLAUDE LABJP
●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/Claude Code
⟐ Claude Code/2026-05-18Intermediate

Why Your `cd` and `export` Vanish Between Claude Code Bash Calls

Claude Code's Bash tool runs each call in a fresh shell, so cd and export never persist. Here's the symptom, the cause, and five practical patterns I use across my Dolice Labs pipelines.

Claude Code264Bash5ShellTroubleshooting20Automation45

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 repo

export behaves the same way:

# Call #1
export GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxx
# Call #2
echo "$GITHUB_TOKEN"
# → empty

What 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 main

It'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/ja

git -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 main

This 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.git

Hardcoding 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.net

Variables 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" main

For 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.sh

set -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:

  1. Every Bash call should be safely re-runnable on its own. Makes partial reruns and debugging much easier.
  2. State travels via files or path arguments, not variables. A tempfile under /tmp, or a fully qualified path passed in.
  3. Anywhere you'd write cd, replace it with && chaining or git -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 &&?"

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⟐ Claude Code2026-10-13
The Same Approval Prompt for 'cd, git add, git commit' Every Time — Following It to a Design Choice, Not a Missing Setting
With a compound command like cd /path && git add && git commit, Claude Code keeps asking for approval even with allow rules in place. Reading the official permissions page next to a GitHub report shows that cd combined with git prompts by design. Here is how to tell, in what order to check, and four ways to cut the prompts.
⟐ Claude Code2026-09-01
Drop the quotes on a heredoc and the prices in your log quietly change
An unquoted heredoc runs the variables and backticks inside its body. Here are the four ways my log got rewritten, how the three quoting forms compare, a safe placeholder-and-sed pattern, and a small script for auditing what you already have.
⟐ Claude Code2026-05-20
Claude Code Says 'All Tests Pass' but CI Goes Red — Designing Around Exit Code Pitfalls
Locally, Claude Code reports 'all tests pass.' You push it, and GitHub Actions turns red within minutes. The root cause is usually not Claude Code itself but how the shell and the test runner interpret exit codes. Here is a practical, experience-backed walkthrough.