Env Vars in Claude Code Sub-Agent Chain
This article was written based on Claude Code v2.0.55.
Claude Code's sub-agent system is powerful for breaking down complex tasks, but there's a gap: environment variables set in a parent agent don't automatically propagate to child sub-agents. This article presents a hooks-based solution that enables environment variable inheritance across the entire agent chain.
The Problem
Consider this scenario: you ask Claude Code to set an environment variable and then spawn a sub-agent, expecting the sub-agent to read that variable:
USER:
"Hi! Please set env var MYSTERY to 42 and then spawn a sub-agent and ask them
to check what value they have in their MYSTERY env var."USER:
"Hi! Please set env var MYSTERY to 42 and then spawn a sub-agent and ask them
to check what value they have in their MYSTERY env var."The sub-agent won't see MYSTERY=42. Sub-agents are separate processes — when Claude Code spawns one, it creates a new instance with a fresh environment. Variables set in the parent exist only in that parent's memory space and don't propagate to children created later.
This creates friction in agent orchestration. Without variable inheritance, passing context and parameters to sub-agents requires either including them in downstream prompts or adding instructions to sub-agent workflows — both of which waste inference time and tokens.
The Solution: A File-Based Environment Stack
The key insight is to use the filesystem as a shared state mechanism, combined with Claude Code's hooks system to manage the lifecycle. Here's the high-level approach:
- A working directory (
.claude/env-vars/) stores environment variable files - A helper script writes variables to the current env file
- Hooks manage the stack of env files and inject them into Bash commands
Hooks are shell commands that Claude Code executes automatically at specific lifecycle events — when the user submits a prompt, before a tool runs, or when a sub-agent completes. They can inspect and modify tool inputs, block actions, or perform side effects. This makes them ideal for injecting our environment variable logic at exactly the right moments.
The "stack" is crucial for nested sub-agents. When a root agent spawns a sub-agent (level 1), which then spawns another sub-agent (level 2), we need separate env files for each level. The stack ensures that:
- Each agent writes to its own file (avoiding conflicts)
- All agents see variables from parent agents (via sourcing all files in order)
- When an agent completes, only its file is removed (preserving parent state)
Implementation
Directory Structure
your-project/
.claude/
settings.local.json
env-vars/
env-vars1
env-vars2
...
hooks/
user-prompt-submit.sh
pre-tool-use-task.sh
pre-tool-use-bash.sh
subagent-stop.sh
setenv.sh
Hook Configuration
In .claude/settings.local.json:
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "./.claude/hooks/user-prompt-submit.sh"
}
]
}
],
"PreToolUse": [
{
"matcher": "Task",
"hooks": [
{
"type": "command",
"command": "./.claude/hooks/pre-tool-use-task.sh"
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "./.claude/hooks/pre-tool-use-bash.sh"
}
]
}
],
"SubagentStop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "./.claude/hooks/subagent-stop.sh"
}
]
}
]
}
}{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "./.claude/hooks/user-prompt-submit.sh"
}
]
}
],
"PreToolUse": [
{
"matcher": "Task",
"hooks": [
{
"type": "command",
"command": "./.claude/hooks/pre-tool-use-task.sh"
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "./.claude/hooks/pre-tool-use-bash.sh"
}
]
}
],
"SubagentStop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "./.claude/hooks/subagent-stop.sh"
}
]
}
]
}
}The hooks serve distinct purposes:
- UserPromptSubmit: Fires when the user sends a message; we use it to reset state for new conversations
- PreToolUse(Task): Fires before spawning a sub-agent; we push a new env file onto the stack
- PreToolUse(Bash): Fires before every Bash command; we inject env vars by modifying the command
- SubagentStop: Fires when a sub-agent completes; we pop its env file from the stack
The Hooks
1. UserPromptSubmit Hook - Session Reset
This hook clears the working directory when a new user prompt is submitted, ensuring a fresh start:
#!/bin/bash
# Hook: UserPromptSubmit - clears env-vars directory
if [ -d ".claude/env-vars" ]; then
rm -rf .claude/env-vars
fi#!/bin/bash
# Hook: UserPromptSubmit - clears env-vars directory
if [ -d ".claude/env-vars" ]; then
rm -rf .claude/env-vars
fiThis reset ensures that each new conversation starts with a clean slate.
2. PreToolUse(Task) Hook - Stack Push
When a sub-agent is spawned, create a new env file for that level:
#!/bin/bash
# Hook: PreToolUse(Task) - adds new env file for sub-agent
if [ ! -d ".claude/env-vars" ]; then
exit 0
fi
# Find the last env file number and create the next one
last_file=$(ls -1 .claude/env-vars/env-vars* 2>/dev/null | sort -V | tail -1)
if [ -z "$last_file" ]; then
next_num=1
else
last_num=$(basename "$last_file" | sed 's/env-vars//')
next_num=$((last_num + 1))
fi
touch ".claude/env-vars/env-vars${next_num}"#!/bin/bash
# Hook: PreToolUse(Task) - adds new env file for sub-agent
if [ ! -d ".claude/env-vars" ]; then
exit 0
fi
# Find the last env file number and create the next one
last_file=$(ls -1 .claude/env-vars/env-vars* 2>/dev/null | sort -V | tail -1)
if [ -z "$last_file" ]; then
next_num=1
else
last_num=$(basename "$last_file" | sed 's/env-vars//')
next_num=$((last_num + 1))
fi
touch ".claude/env-vars/env-vars${next_num}"3. PreToolUse(Bash) Hook - Environment Injection
This hook prepends sourcing of all env files to every Bash command:
#!/bin/bash
# Hook: PreToolUse(Bash) - injects env vars into commands
if [ ! -d ".claude/env-vars" ]; then
exit 0
fi
envvars_files=$(ls -1 .claude/env-vars/env-vars* 2>/dev/null | sort -V)
if [ -z "$envvars_files" ]; then
exit 0
fi
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')
if [ -z "$command" ]; then
exit 0
fi
# Build source prefix for all env files
source_prefix=""
for envvars_file in $envvars_files; do
source_prefix="${source_prefix}set -a; source \"$envvars_file\"; set +a; "
done
new_command="${source_prefix}${command}"
jq -n --arg cmd "$new_command" '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {"command": $cmd}
}
}'#!/bin/bash
# Hook: PreToolUse(Bash) - injects env vars into commands
if [ ! -d ".claude/env-vars" ]; then
exit 0
fi
envvars_files=$(ls -1 .claude/env-vars/env-vars* 2>/dev/null | sort -V)
if [ -z "$envvars_files" ]; then
exit 0
fi
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')
if [ -z "$command" ]; then
exit 0
fi
# Build source prefix for all env files
source_prefix=""
for envvars_file in $envvars_files; do
source_prefix="${source_prefix}set -a; source \"$envvars_file\"; set +a; "
done
new_command="${source_prefix}${command}"
jq -n --arg cmd "$new_command" '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {"command": $cmd}
}
}'If a child agent redefines a parent's variable, the child's value takes precedence. Since we source files in order (parent first, then children), later assignments override earlier ones.
4. SubagentStop Hook - Stack Pop
When a sub-agent completes, remove its env file:
#!/bin/bash
# Hook: SubagentStop - removes the sub-agent's env file
if [ ! -d ".claude/env-vars" ]; then
exit 0
fi
last_file=$(ls -1 .claude/env-vars/env-vars* 2>/dev/null | sort -V | tail -1)
if [ -n "$last_file" ]; then
rm "$last_file"
fi#!/bin/bash
# Hook: SubagentStop - removes the sub-agent's env file
if [ ! -d ".claude/env-vars" ]; then
exit 0
fi
last_file=$(ls -1 .claude/env-vars/env-vars* 2>/dev/null | sort -V | tail -1)
if [ -n "$last_file" ]; then
rm "$last_file"
fiThis implements a classic stack "pop" operation — the last file always belongs to the most recently spawned sub-agent.
The Setenv Script
A helper script to write variables to the current agent's env file:
#!/bin/bash
# Sets a variable in the current agent's env file
# Usage: ./setenv.sh VAR_NAME "value"
if [ $# -lt 2 ]; then
echo "Usage: $0 VAR_NAME value" >&2
exit 1
fi
var_name="$1"
var_value="$2"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
mkdir -p "$SCRIPT_DIR/.claude/env-vars"
last_file=$(ls -1 "$SCRIPT_DIR/.claude/env-vars"/env-vars* 2>/dev/null | sort -V | tail -1)
if [ -z "$last_file" ]; then
last_file="$SCRIPT_DIR/.claude/env-vars/env-vars1"
touch "$last_file"
fi
if grep -q "^${var_name}=" "$last_file" 2>/dev/null; then
sed -i "s|^${var_name}=.*|${var_name}=${var_value}|" "$last_file"
else
echo "${var_name}=${var_value}" >> "$last_file"
fi#!/bin/bash
# Sets a variable in the current agent's env file
# Usage: ./setenv.sh VAR_NAME "value"
if [ $# -lt 2 ]; then
echo "Usage: $0 VAR_NAME value" >&2
exit 1
fi
var_name="$1"
var_value="$2"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
mkdir -p "$SCRIPT_DIR/.claude/env-vars"
last_file=$(ls -1 "$SCRIPT_DIR/.claude/env-vars"/env-vars* 2>/dev/null | sort -V | tail -1)
if [ -z "$last_file" ]; then
last_file="$SCRIPT_DIR/.claude/env-vars/env-vars1"
touch "$last_file"
fi
if grep -q "^${var_name}=" "$last_file" 2>/dev/null; then
sed -i "s|^${var_name}=.*|${var_name}=${var_value}|" "$last_file"
else
echo "${var_name}=${var_value}" >> "$last_file"
fiDon't forget to make this script executable: chmod +x ./setenv.sh. All hook scripts in .claude/hooks/ also need execute permissions.
Example
Here's a conversation demonstrating the environment variable inheritance:
USER:
"Set MYSTERY to 42 using setenv.sh, then spawn a sub-agent. The sub-agent
should check the MYSTERY value, set ANOTHER to 67, then complete. After that,
check if ANOTHER is still available."
AGENT:
"I'll set MYSTERY and spawn a sub-agent."
$ ./setenv.sh MYSTERY 42
"Now spawning a sub-agent to check MYSTERY and set ANOTHER..."
SUB-AGENT:
$ echo $MYSTERY
42
$ ./setenv.sh ANOTHER 67
$ echo $ANOTHER
67
# Task complete.
AGENT:
"The sub-agent confirmed MYSTERY=42 and set ANOTHER=67. Let me check if
ANOTHER persists:"
$ echo $ANOTHER
# (empty)USER:
"Set MYSTERY to 42 using setenv.sh, then spawn a sub-agent. The sub-agent
should check the MYSTERY value, set ANOTHER to 67, then complete. After that,
check if ANOTHER is still available."
AGENT:
"I'll set MYSTERY and spawn a sub-agent."
$ ./setenv.sh MYSTERY 42
"Now spawning a sub-agent to check MYSTERY and set ANOTHER..."
SUB-AGENT:
$ echo $MYSTERY
42
$ ./setenv.sh ANOTHER 67
$ echo $ANOTHER
67
# Task complete.
AGENT:
"The sub-agent confirmed MYSTERY=42 and set ANOTHER=67. Let me check if
ANOTHER persists:"
$ echo $ANOTHER
# (empty)As expected, ANOTHER is not available. When the sub-agent completed, its env file (containing ANOTHER=67) was removed by the SubagentStop hook. Only the parent's variables persist.
Conclusion
We achieved transparent environment variable inheritance across the agent hierarchy without modifying prompts or wasting tokens on parameter passing. The stack-based approach correctly handles nested sub-agents and ensures proper cleanup when agents complete.
Note that this solution is simplified and supports only one Claude Code session at a time.