Making Claude Code Tell You What It's Doing
Adrian Sutton
Claude Code has a status line that sits at the bottom of the terminal showing things like the current directory, git branch, model, and context window usage. It’s driven by a shell script that receives JSON on stdin and prints whatever it wants. I wanted to add one more thing: a short description of what the session is actually working on.
The Simple Way: /rename
The built-in /rename command sets a session name that Claude Code displays above the prompt. Type /rename fix auth bug at the start of each session and you’re done — no scripts needed.
The downside is that it’s manual, and /rename can’t be invoked programmatically by Claude. If you want Claude to automatically describe what it’s working on and update that description as the focus shifts, you need the automated approach below.
The Automated Approach
The goal is for Claude to write a short status like “fix auth bug” that shows up in the status line, updated automatically as the session’s focus changes:
op-claude (main) Opus ctx:8% · fix auth bug
This turns out to be harder than it should be. The status line script receives a JSON blob on stdin that includes the session_id. Claude’s bash tool calls don’t. There’s no $SESSION_ID environment variable, and $PPID differs between the two because they’re spawned through different process trees.
So we need a way for the status line side (which knows the session ID) to leave a breadcrumb that the bash side (which doesn’t) can find.
The Breadcrumb
Both the status line script and Claude’s bash calls have a common ancestor: the claude process. They just reach it through different paths. The trick is to walk up the process tree until you find a process named claude, then use its PID as a shared key.
A UserPromptSubmit hook runs on every user message and receives the session_id in its input. It walks the process tree to find the ancestor claude PID and writes a breadcrumb file mapping one to the other:
#!/usr/bin/env bash
# ~/.claude/hooks/session-status.sh
input=$(cat)
session_id=$(echo "$input" | jq -r '.session_id // empty')
[ -z "$session_id" ] && exit 0
# Write breadcrumb mapping ancestor claude PID -> session_id
pid=$PPID
while [ "$pid" -gt 1 ]; do
comm=$(ps -o comm= -p "$pid" 2>/dev/null)
if [ "$comm" = "claude" ]; then
echo "$session_id" > "/tmp/claude-sid-${pid}"
break
fi
pid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')
done
# If no status file exists yet, remind Claude to create one
if [ -f "/tmp/claude-status-${session_id}" ]; then
exit 0
fi
jq -n '{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "STATUS LINE REMINDER: Run ~/.claude/update-status.sh \"short summary\" to set what this session is working on (under 30 chars)."
}
}'
That last part is important. You can’t just tell Claude in your CLAUDE.md to “please update the status line” and expect it to reliably happen. The hook injects a reminder into the conversation context on every user message until a status file exists. Belt and suspenders.
Writing the Status
Claude calls a small helper script that does the same process-tree walk in reverse — finds the claude ancestor PID, reads the breadcrumb to get the session ID, then writes the status:
#!/usr/bin/env bash
# ~/.claude/update-status.sh "short summary"
msg="$1"
[ -z "$msg" ] && exit 1
pid=$$
while [ "$pid" -gt 1 ]; do
comm=$(ps -o comm= -p "$pid" 2>/dev/null)
if [ "$comm" = "claude" ]; then
sid=$(cat "/tmp/claude-sid-${pid}" 2>/dev/null)
[ -n "$sid" ] && echo "$msg" > "/tmp/claude-status-${sid}"
exit 0
fi
pid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')
done
The Status Line Script
The full status line script reads the JSON from stdin, extracts the fields it cares about, and builds the output. The session status is just another part appended at the end:
#!/usr/bin/env bash
# ~/.claude/statusline-command.sh
input=$(cat)
cwd=$(echo "$input" | jq -r '.cwd // .workspace.current_dir // ""')
model=$(echo "$input" | jq -r '.model.display_name // ""')
used_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
vim_mode=$(echo "$input" | jq -r '.vim.mode // empty')
session_id=$(echo "$input" | jq -r '.session_id // empty')
# Per-session status from temp file keyed by session_id
session_status=""
if [ -n "$session_id" ]; then
session_status=$(cat "/tmp/claude-status-${session_id}" 2>/dev/null || true)
fi
# Directory: basename of cwd
dir=$(basename "$cwd")
# Git branch (skip optional locks)
branch=""
if git_out=$(GIT_OPTIONAL_LOCKS=0 git -C "$cwd" symbolic-ref --short HEAD 2>/dev/null); then
branch="$git_out"
fi
# Build status line parts
parts=()
# Directory in cyan
parts+=("$(printf '\033[36m%s\033[0m' "$dir")")
# Git branch in yellow if present
if [ -n "$branch" ]; then
parts+=("$(printf '\033[33m(%s)\033[0m' "$branch")")
fi
# Model
if [ -n "$model" ]; then
parts+=("$(printf '\033[90m%s\033[0m' "$model")")
fi
# Context usage with color thresholds
if [ -n "$used_pct" ]; then
used_int=${used_pct%.*}
if [ "$used_int" -ge 80 ] 2>/dev/null; then
color='\033[31m'
elif [ "$used_int" -ge 50 ] 2>/dev/null; then
color='\033[33m'
else
color='\033[32m'
fi
parts+=("$(printf "${color}ctx:%s%%\033[0m" "$used_int")")
fi
# Session status (per-session work summary)
if [ -n "$session_status" ]; then
parts+=("$(printf '\033[90m· %s\033[0m' "$session_status")")
fi
# Vim mode
if [ -n "$vim_mode" ]; then
parts+=("$(printf '\033[90m[%s]\033[0m' "$vim_mode")")
fi
printf '%s' "${parts[*]}"
Wiring It Up
Make both scripts executable:
chmod +x ~/.claude/statusline-command.sh ~/.claude/update-status.sh ~/.claude/hooks/session-status.sh
Register the status line and hook in ~/.claude/settings.json:
{
"statusLine": {
"type": "command",
"command": "bash ~/.claude/statusline-command.sh"
},
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/session-status.sh"
}
]
}
]
}
}
And add the instruction to your CLAUDE.md that tells Claude when to update:
## Session Status Line
Update the session status line so the user can see what each session
is working on at a glance.
- **After the first user prompt**: run `~/.claude/update-status.sh "short summary"`
as part of your first response
- **Periodically**: run it again every ~5 interactions or when focus shifts
- Keep summaries under 30 chars
What I Learned
The interesting constraint here is that Claude Code’s extensibility points — status line scripts, hooks, and bash tool calls — all run as separate processes with no shared environment. There’s no session ID in the environment, no shared memory, no IPC channel. The process tree walk is a hack, but it’s a reliable one. Every subprocess of a Claude Code session shares a common claude ancestor, even if the paths diverge.
The other lesson is that CLAUDE.md instructions alone aren’t enough for “always do X” behaviors. Claude follows them inconsistently, especially across sessions. Hooks that inject reminders into the conversation context are much more reliable. The CLAUDE.md instruction tells Claude what to do; the hook makes sure it actually does it.