SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-29

How to Set Claude Code Statusline on VPS

Claude Code statusLine runs a script through stdin and stdout. Show hostname, directory, git branch and model under your prompt, so you edit the right 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 a statusLine block to settings.json, then point am to one command. Claude Code go run that command, send session state give am as JSON through standard input, and 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 send to the model, so e no dey use any token.

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 wey no get labels fit make migration enter wrong machine. Statusline wey start with hostname go stop this kind mistake.

Where statusLine setting dey inside settings.json

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

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

type na always "command". command value dey run through a shell, so e fit be script path or plain command. Confirm say the connection 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, not your script. Read "Why statusline dey blank" below.

As of August 2026, three optional keys dey available. padding dey add horizontal space measured for characters, and e default to 0. refreshInterval dey run the command again every N seconds on top 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 hide the built-in -- INSERT -- text when your own script already dey render the vim mode.

Statusline script dey receive which data?

No trust any field list wey you read anywhere, including this page. Capture the real object wey your version dey send. Write temporary 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 structure 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 get id and display_name inside am. workspace get current_dir and project_dir: current_dir na where the session dey now, project_dir na where dem launch am, and both values go differ once working directory change during session. The top-level cwd carry the same value as workspace.current_dir. context_window get token counts plus pre-calculated used_percentage. cost get total_cost_usd and duration counters. session_id no change throughout the session and e 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 go show only when the matching feature dey active. If vim mode off and you read .vim.mode with jq -r, e go print the literal string null, and your bar go show null to reader. Add // empty to every selector, so missing key go print nothing.

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

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

Statusline script wey go degrade instead of breaking

Dis 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 rename or dem remove am, e go produce empty string, and next line go supply default. Directory dey fall back from workspace.current_dir to cwd to $PWD. Branch dey come from git -C "$DIR" instead of bare git, so branch always match directory wey bar dey show.

Save am, then make am executable:

chmod +x ~/.claude/statusline.sh

Execute bit no be optional. Claude Code dey run the command through shell, so script wey no get +x go fail with Permission denied, e no go produce stdout, and 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 twice by hand. First with a normal session object:

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

You go get 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 the 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 dey print: the hostname, the current directory from $PWD, and the word claude for where the model name suppose dey. Nothing crash and nothing print null. Script wey pass this test go survive if dem rename one field, because to your script, renamed field and missing field na the same event.

Wetin you suppose see

The statusline dey render for e own row above the built-in footer badges, and e no replace dem. For correct setup, e go be one row: short hostname for cyan, then working directory with your home directory collapse to ~, then branch name for yellow when the directory na git repository, then model name dimmed. E suppose resemble 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 enter, 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 one burst of changes go run the script once. The bar go hide during autocomplete, help menu, and permission prompts, then e go return.

Why hostname need come first

When you dey 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 a tmux pane and the window title often still dey show the old name, because na shell wey never know say e move set the title. If you leave Claude Code running for detached tmux session on VPS and attach again one day later, nothing for screen go separate the build server from the production box.

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

Give each server im own colour so you fit recognise am before you read am. Put two lines 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 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.

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

Small screen make all this more obvious, because you no get window title to fall back on. If na your setup, see using Claude Code from phone.

Make the script run fast

Your script dey run for every assistant message, and Claude Code dey cancel run wey still dey process when new update arrive. So, if script slow, e fit show old text or no text at all.

Each jq call dey cost few milliseconds. git na the part wey dey slow: git status for large repository with cold cache fit take hundreds of milliseconds. The script above avoid git status on purpose and call git branch --show-current, wey 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 as the 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 change 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 different between sessions. So, two Claude Code sessions for two repositories no fit read each other's cached branch name. Sessions dey remain that isolated by design. To make one session hand work give another one, you need take deliberate step. Na wetin send message from one Claude Code session to another dey do be that.

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.

Wetin make statusline remain 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 wey dey above. If e print one line for shell but e no show for Claude Code, start with claude --debug, wey dey log exit code and stderr of the first statusline run for the session.

The debug log talk 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 dey 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 dey print null. A jq selector reach key wey no dey or wey be 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 branch empty and carry the whole script exit code 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 also 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 no fit handle the overlap. Keep the output short. For proper accounting of usage instead of number for bar, see how Claude Code dey count tokens.

FAQ

Claude Code statusline setting dey live for where?

E dey inside 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 dem apply to every project for that machine. Project settings dey for .claude/settings.json inside the repository, and dem get priority for that directory. Settings reload by demself, but change go only show for the next update trigger, like your next message.

Why my Claude Code statusline blank?

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

Statusline JSON include git branch?

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

Statusline dey cost tokens or slow session?

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

How I fit show different statusline for each server?

Keep one script and make am read the machine. The script above print $HOSTNAME with hostname -s as fallback, so the same file wey you copy go every box go label each one correctly. The checksum colour trick also give each 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.