SSD Nodes Learn 🎉 VPS from $5.50/mo
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-13

How I Set Claude Code Statusline for VPS

Set Claude Code statusLine to run a script, then show hostname, directory, git branch and model under your prompt, so you no edit the wrong VPS.

Wetin Claude Code statusline dey show

Claude Code statusline na one row under prompt wey dey display wetin script wey you write produce. You go add statusLine block to settings.json and point am to one command. Claude Code go run that command, send session state give am as JSON through standard input, then print anything wey the command write to standard output.

Na so the contract complete be. Your script go read JSON from stdin and print text to stdout. E dey run for your machine, and nothing wey e print dey go to the model, so e no dey use tokens.

For laptop wey get one project, na decoration be this. For three servers, e be safety rail. Every Claude Code session dey look the same for every terminal, so four SSH windows without labels fit make migration enter wrong server. Statusline wey start with hostname dey stop this kind mistake.

Where statusLine setting dey for settings.json

Put am for your user settings at ~/.claude/settings.json. E go apply to every project for that machine. Project settings for .claude/settings.json inside repository dey work too, and dem get priority for that directory.

{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline.sh"
  }
}

type always na "command". The command value dey run through a shell, so e fit be script path or plain command. Confirm say the wiring dey work before you write any script:

{
  "statusLine": {
    "type": "command",
    "command": "hostname -s"
  }
}

Start Claude Code and send one message. The bar under the prompt go now show the server short hostname. If e remain empty, the problem dey with the setting or the trust dialog, no be your script. Read "Why statusline dey remain blank" below.

Three optional keys dey available as of August 2026. padding dey add horizontal spacing for characters and e default to 0. refreshInterval dey run the command again every N seconds on top of the normal triggers, with minimum of 1. Use am only when the line dey show clock or something wey dey change while the session dey idle. hideVimModeIndicator dey suppress the built-in -- INSERT -- text when your own script already dey render the vim mode.

Wetin data statusline script dey receive?

No trust field list wey you read anywhere, including this page. Capture the real object wey your version dey send. Write throwaway script wey go save stdin to file:

cat > ~/.claude/statusline-capture.sh <<'EOF'
#!/bin/bash
cat > /tmp/statusline-input.json
echo "captured"
EOF
chmod +x ~/.claude/statusline-capture.sh

Point statusLine.command to that file, start session, and send one message. The bar dey read captured. Now check wetin arrive:

jq . /tmp/statusline-input.json

You get the exact shape for your build, and you fit repeat am anytime update change something.

The stable parts, as dem document am for August 2026, na nested objects instead of flat keys. model hold id and display_name. workspace hold current_dir and project_dir: current_dir na where session dey now, project_dir na where dem launch am from, and both go differ once working directory change during session. Top-level cwd carry the same value as workspace.current_dir. context_window hold token counts plus pre-calculated used_percentage. cost hold total_cost_usd and duration counters. session_id stable for the whole life of session and unique across sessions, wey matter for caching later.

Three rules fit keep script working when schema change.

Some keys dey absent, no be null. vim, agent, pr, worktree and effort dey appear only when the matching feature active. If you read .vim.mode with jq -r while vim mode dey off, e go print literal string null, and your bar go show null to reader. Append // empty to every selector, so missing key go print nothing.

Some values dey null early. context_window.used_percentage and context_window.current_usage dey null before first API response, and current_usage return to null after /compact until next call populate am again. Context percentage for bar therefore need // 0, or e go read null for the first seconds of every session. Before you put that number for bar, e help to know how context window dey fill up.

Git branch no dey inside JSON. No field report am. Any branch wey dey your bar come from your script wey run git by itself.

Statusline script wey go degrade instead of break

This na the copy-paste version. E dey print hostname, working directory, git branch and model name. Every field get fallback, so even empty JSON object still go produce usable line.

#!/bin/bash
# ~/.claude/statusline.sh
input=$(cat)

# Read one field. Prints nothing when the key is missing or null.
field() { printf '%s' "$input" | jq -r "$1 // empty" 2>/dev/null; }

HOST=$(hostname -s 2>/dev/null)
[ -z "$HOST" ] && HOST="host"

DIR=$(field '.workspace.current_dir')
[ -z "$DIR" ] && DIR=$(field '.cwd')
[ -z "$DIR" ] && DIR="$PWD"

MODEL=$(field '.model.display_name')
[ -z "$MODEL" ] && MODEL="claude"

SHORT="$DIR"
if [ -n "$HOME" ]; then
  case "$DIR" in
    "$HOME") SHORT="~" ;;
    "$HOME"/*) SHORT="~/${DIR#"$HOME"/}" ;;
  esac
fi

BRANCH=""
if git -C "$DIR" rev-parse --git-dir >/dev/null 2>&1; then
  BRANCH=$(git -C "$DIR" branch --show-current 2>/dev/null)
  [ -z "$BRANCH" ] && BRANCH="detached"
fi

CYAN=$'\033[36m'
YELLOW=$'\033[33m'
DIM=$'\033[2m'
RESET=$'\033[0m'

LINE="${CYAN}${HOST}${RESET} ${SHORT}"
[ -n "$BRANCH" ] && LINE="${LINE} ${YELLOW}${BRANCH}${RESET}"
LINE="${LINE} ${DIM}${MODEL}${RESET}"

printf '%s\n' "$LINE"

Every read dey pass through field, wey dey append // empty. So if key don change name or dem remove am, e go produce empty string, and the next line go supply default. The directory fallback dey move from workspace.current_dir to cwd to $PWD. The branch dey come from git -C "$DIR" instead of bare git, so the branch always match the directory wey the bar dey show.

Save am, then make am executable:

chmod +x ~/.claude/statusline.sh

The execute bit no be optional. Claude Code dey run the command through shell, so script without +x go fail with Permission denied, e no go produce stdout, and the row go remain blank without any visible error.

jq dey parse JSON for command line, and e no dey installed for fresh Ubuntu server:

sudo apt update && sudo apt install -y jq

Then point the setting to the script, using the first settings.json block above.

Test the script before you trust am

Run am two times by hand. First, use normal session object:

echo '{"model":{"display_name":"Opus"},"workspace":{"current_dir":"/srv/api"},"session_id":"t1"}' | ~/.claude/statusline.sh

You go see the hostname, then /srv/api, then Opus. No branch go show, because /srv/api for your machine probably no be git repository.

Second one na degradation test, and na this one people dey skip:

echo '{}' | ~/.claude/statusline.sh

Empty object na the worst case wey schema change fit give you. The line still prints: the hostname, the current directory from $PWD, and the word claude where the model name suppose dey. Nothing crash and nothing print null. Script wey pass this test fit survive if dem rename one field, because for your script, renamed field and missing field na the same event.

Wetin you suppose see

The statusline dey render for im own row above the built-in footer badges, and e no replace dem. For correct setup, na one row: short hostname for cyan, then working directory with your home directory collapsed to ~, then branch name for yellow when the directory na git repository, then model name wey dim. E go look something close to web-01 ~/api main Opus, with those four parts get different colours.

The row go run your script again when session start, including resume, when new assistant message arrive, after /compact finish, when permission mode change, when vim mode toggle, and for every refreshInterval tick if you set one. Updates get 300 ms debounce, so if plenty changes happen together, script go run once. The bar dey hide during autocomplete, help menu and permission prompts, then e go return.

Why hostname dey first

When you keep agents running for more than one server, na terminal alone dey tell you where you dey, and terminals fit lie. Open another ssh connection from inside one tmux pane, and the window title often still keep the old name, because na shell wey never know say e move set the title. Leave Claude Code dey run for detached tmux session on VPS and reattach one day later, and nothing for screen go separate build server from production box.

Statusline dey different because na Claude Code itself dey render am, per session, from data wey that session hold. E no fit inherit wrong pane information or remain stale because shell prompt never refresh. Wetin e show na the box wey agent dey write files on.

Give every server e own colour so you go recognise am before you read am. Two lines, put dem above the LINE= assignment:

CODE=$(printf '%s' "$HOST" | cksum | cut -d' ' -f1)
HOST_COLOR=$(printf '\033[%dm' "$((31 + CODE % 6))")

Then use ${HOST_COLOR} instead of ${CYAN}. cksum go print checksum of hostname, so one given name always map to the same colour for range 31 to 36, wey be red reach cyan. Copy the same script go every box, and each one go label itself.

Directory deserve e place for the same reason. /srv/api and /srv/api-staging dey one keystroke apart inside ssh command, but their effect fit be one whole incident apart. Model and branch na the other two wey worth the space: model dey tell you which session you resume, and branch dey tell you whether agent dey about to commit onto main.

Small screen make all this more important, because window title no dey available as backup. If na your setup, see how to control Claude Code from phone.

Make script dey fast

Your script dey run for every assistant message, and Claude Code dey cancel run wey still dey go whenever new update come in. So, slow script fit show old text, or e no show anything.

Each jq call dey cost some milliseconds. git na the part wey dey slow: git status for large repository with cold cache fit take hundreds of milliseconds. The script above intentionally avoid git status and call git branch --show-current, wey dey read .git/HEAD and return immediately.

If you add anything wey heavy pass, cache am for file and refresh am every few seconds. Use session key for the file:

CACHE="/tmp/statusline-$(field '.session_id')"

Use session_id, no be $$. $$ na the process ID of your script, and e dey different for every invocation. So, cache wey use am as key no go ever hit, and you go pay the full cost every time. session_id dey stable for the whole session and e dey different between sessions. This mean say two Claude Code sessions for two repositories no fit read each other's cached branch name.

One more limit wey you suppose know: tput cols no dey work inside statusline script. Claude Code dey capture the output instead of attaching your script to the terminal, so width detection no get anything to measure. Claude Code dey set the COLUMNS and LINES environment variables before e run the command, for v2.1.153 and later. So read $COLUMNS when you need decide how much text to print.

Why statusline dey blank

Nothing dey show at all. Check the execute bit with ls -l ~/.claude/statusline.sh, then run the script by hand with the mock input above. If e print one line for shell but e no show for Claude Code, start with claude --debug, wey dey log the exit code and stderr of the first statusline run for the session.

The debug log say Status line command skipped: workspace trust not accepted. Statusline dey execute shell command, so e dey behind the same workspace trust gate like hooks. Until you accept the trust dialog for that directory, the command no go run. This one common for VPS, where every new clone na directory wey Claude Code never see before. Restart Claude Code for that directory and accept the dialog.

Everything blank and disableAllHooks dey set. "disableAllHooks": true for settings.json dey disable statusline too, because na the same shell-execution gate. Remove am or set am to false.

The row print null. A jq selector reach key wey no dey or null, and jq -r dey print null as the four characters null. Add // empty for text and // 0 for numbers.

The row go blank immediately after you edit the script. Command wey exit non-zero, or wey print nothing, go blank the row. The usual cause na final line like [ -n "$BRANCH" ] && LINE="...", wey dey exit 1 when the branch empty and carry the whole script exit code go with am. Keep printf last, or add exit 0.

Escape codes dey show as literal text like \e]8;; for the bar. Use printf '%b' instead of echo -e. Clickable OSC 8 links still need terminal wey support dem, and tmux or SSH fit strip the sequences, so plain colour na the safer choice for remote box.

The right side of the row dey cut off. System notifications and the verbose-mode token counter dey share that row from the right, and narrow terminal dey lose the overlap. Keep the output short. For real accounting of usage instead of number for bar, see how Claude Code dey count tokens.

FAQ

? Where does the Claude Code statusline setting live?

E dey for settings.json, as a statusLine block wey type set to "command", and command set to script path or shell command. User settings dey for ~/.claude/settings.json and e apply to every project for that machine. Project settings dey for .claude/settings.json inside repository, and dem get priority for that directory. Settings dey reload by demself, but change no go show until the next update trigger, like your next message.

Why is my Claude Code statusline blank?

Four causes cover almost everything. Script no get execute bit, so shell dey return Permission denied and nothing dey reach stdout. You never accept workspace trust dialog, and claude --debug dey log Status line command skipped: workspace trust not accepted. disableAllHooks na true, and this one disable statusline under the same gate. Or script dey exit with non-zero status, wey make the row blank. Test am by hand first: echo '{}' | ~/.claude/statusline.sh suppose print something.

Does the statusline JSON include the git branch?

No. The JSON carry session state like model, workspace directories, context window numbers, and cost. Nothing inside am report git. Branch wey show for your bar come from your own script wey call git branch --show-current. Pass the directory from JSON with git -C "$DIR", so branch always match the directory wey the bar dey show.

Does a statusline cost tokens or slow the session down?

E no cost tokens, because script dey run locally and e output never dey send to model. Na you dey responsible for speed. The command dey run for every assistant message with 300 ms debounce, and Claude Code dey cancel any run wey still dey execute when new update enter. So, if script take full second, e go show old text. Avoid git status for large repositories, and cache anything wey slow inside file keyed on session_id.

How do I show a different statusline on each server?

Use one script and make am read the machine. The script above dey print $HOSTNAME, with hostname -s as fallback. So, the same file wey you copy go every box go label each one correctly, and checksum colour trick go give every hostname e own colour. If one server need different layout, put statusLine block inside project settings of the repository wey you dey work on for that box, because project settings override user settings for that directory.