Builds a Claude Code statusline from scratch or extends an existing one. A statusline is a live status bar that runs a shell script after every Claude response and displays data — security scans, git status, API quotas, build state — directly in the session. Use this skill when the user says "create a statusline", "make a statusline for X", "add X to my statusline", "show Y in the status bar", "build a statusline plugin", or "I want live data in my Claude session". Trigger even if the user just describes wanting live feedback during a Claude session without naming "statusline" explicitly. The skill drives from idea to a working, installed, tested statusline without requiring the user to direct every step.
76
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
A Claude Code statusline is a bash script that Claude runs after every response. It reads session metadata on stdin and prints a colored line to stdout — giving you live, contextual data without leaving the session.
The key design challenge: the statusline must render instantly, every time. There are two valid execution models — choose based on measured speed:
Before choosing a model, benchmark the operation — see Step 1.
Read references/protocol.md at the start of every session — it covers the stdin
JSON schema, the settings.json format, and ANSI output rules. The authoritative
reference for the protocol is the official Claude Code statusline documentation at
https://code.claude.com/docs/en/statusline.md — check it for new fields or behaviors
not yet reflected in the reference files.
Read references/implementation-patterns.md when writing or debugging the script —
it covers the caching pattern, exit code handling, state machine, and jq recipes.
Ask the user what data source they want in their statusline. Common categories:
snyk test, trivy, semgrep, SAST/SCA/IaC scannersFor each data source, establish:
time <command> # run 3× and note the rangeDone when: You have a concrete data source, its command, its output format, and a list of states.
Plan the display format before writing any code. A good statusline line has:
🔒 snyk, 🌿 git, ☁ k8s)│ — one per logical data group· 5m ago)⟳ when a background refresh is runningDesign the full set of states the script will display:
label │ <primary result> │ <secondary result> │ <project> · <age> ⟳
label │ ✔ clean │ project · 2m ago
label │ scanning...
label │ no supported files
label │ ⚠ auth required run: <cmd>Use ANSI RGB colors. Standard palette for security tools (see references/implementation-patterns.md):
\033[38;2;255;85;85m\033[38;2;255;165;0m\033[38;2;255;215;0m\033[38;2;80;250;123m\033[38;2;100;170;255m / \033[38;2;128;128;128mDone when: Every state the script can be in has a corresponding output line designed.
Create statusline.sh in the project root. The structure depends on the execution
model chosen in Step 1 (based on the benchmark results).
If using inline execution (operation benchmarked at <300ms):
TOOL_BIN, display toggles)SESSION=$(cat) — captures Claude's JSON; parse fields as neededprintf the assembled line with separatorsIf using background + cache (operation benchmarked at >300ms or variable):
TOOL_BIN, CACHE_TTL, display toggles)SESSION=$(cat) — captures Claude's JSON; parse fields as neededgit rev-parse --show-toplevel 2>/dev/null || pwdcksum for stable, per-project cache keysfile_age() helper — returns age in seconds of any file; 999999 if missingtrigger_scan_bg() function — atomic mkdir-based lock, runs scan in subshell, captures exit code explicitly, writes noscan sentinel on exit 3printf the assembled line with separatorsSee references/implementation-patterns.md for full patterns for both models.
Critical rules:
|| exit_code=$? to capture exit codes — never || true, which loses them.noscan sentinel rather than retrying indefinitelymkdir for locking (POSIX-atomic), not flock or file touchesjq call per cache file — multiple calls on the same file are slowpwd / git rev-parse to find the projectMake the script executable: chmod +x statusline.sh
Configuration env vars — naming convention: Every user-tunable knob should be an env var with a sane default. Use a consistent naming prefix so vars are discoverable:
# In statusline.sh — configuration block at the top
TOOL_BIN="${TOOL_BIN:-tool}" # path to the CLI binary
TOOL_STATUSLINE_TTL="${TOOL_STATUSLINE_TTL:-300}" # cache TTL in seconds
TOOL_SHOW_LOW="${TOOL_SHOW_LOW:-false}" # boolean display toggle
TOOL_SCAN_ARGS="${TOOL_SCAN_ARGS:-}" # extra args forwarded to scan commandUsers can set these in their shell profile or in ~/.claude/settings.json under "env" — document both paths. The TOOL_BIN var is especially important on macOS where tools installed via nvm, fnm, rbenv, or pyenv are only in PATH in interactive shells; background subshells run by the statusline may not inherit that PATH. Always document the TOOL_BIN override and recommend pinning it if the tool is installed via a version manager:
export TOOL_BIN=$(which tool) # resolve once in your shell profileDone when: The script runs without errors from echo '{}' | ./statusline.sh in the project directory.
Add the statusline to ~/.claude/settings.json. The value must be an object, not a string — a plain string path will cause a settings error on startup:
{
"statusLine": {
"type": "command",
"command": "/absolute/path/to/statusline.sh"
}
}Use jq to update non-destructively (preserves all other settings):
jq --arg p "/absolute/path/to/statusline.sh" \
'.statusLine = {"type": "command", "command": $p}' \
~/.claude/settings.json > ~/.claude/settings.json.tmp \
&& mv ~/.claude/settings.json.tmp ~/.claude/settings.jsonDone when: cat ~/.claude/settings.json shows the statusLine object with the correct absolute path.
Always create an install.sh in the project root. This is not optional — every statusline needs one so users can install and uninstall it without manually editing JSON. Use assets/install.sh.template as the starting point and fill in the placeholders.
A correct installer must:
R/GREEN/YELLOW/RED/BOLD ANSI variables and ok()/info()/die() helpers that use them; use printf not echo; print a bold tool name header at the start~/.claude/settings.json non-destructively using jq--remove to cleanly uninstallAuth preflight — important distinction: there are two different commands for every authenticated tool:
tool whoami, gh auth status) — safe to run anytime; exit 0 = authenticatedtool auth, gh auth login) — only run this if not authenticated; on some tools re-running when already authed resets or replaces credentialsThe installer should run the check command and only instruct the user to run the do-auth command if the check fails. Never silently run the auth command unconditionally.
Make it executable: chmod +x install.sh
Done when: ./install.sh runs cleanly with colored output and ./install.sh --remove cleanly removes the statusLine entry.
Do not rely on the live statusline alone — seed the cache manually to exercise every state:
CACHE_DIR="$HOME/.cache/<tool>-statusline"
HASH=$(printf '%s' "$(git rev-parse --show-toplevel)" | cksum | cut -d' ' -f1)
# Test: clean/no issues
echo '<valid empty result JSON>' > "$CACHE_DIR/${HASH}.json"
echo '{}' | ./statusline.sh
# Test: issues found
echo '<valid result JSON with findings>' > "$CACHE_DIR/${HASH}.json"
echo '{}' | ./statusline.sh
# Test: no supported project
printf '3' > "$CACHE_DIR/${HASH}.noscan"; rm -f "$CACHE_DIR/${HASH}.json"
echo '{}' | ./statusline.sh
# Test: scanning in progress
mkdir "$CACHE_DIR/${HASH}.lock" 2>/dev/null
rm -f "$CACHE_DIR/${HASH}.json" "$CACHE_DIR/${HASH}.noscan"
echo '{}' | ./statusline.sh
rmdir "$CACHE_DIR/${HASH}.lock"
# Test: auth error
echo 'Authentication failed' > "$CACHE_DIR/${HASH}.err"
echo '{}' | ./statusline.shCheck the raw output for correct ANSI escape codes:
echo '{}' | ./statusline.sh | cat -v^[[38;2;...m in the output means ANSI is working correctly.
Repeat testing in the project directory where the data source is valid, and in a directory where no supported files exist, to confirm the noscan path works.
Done when: All designed states produce the expected output line with correct colors and content.
Create a README.md covering:
./install.sh (three steps max)settings.json snippet for users who don't want the installer./install.sh --remove and how to clear the cachesettings.json "env" blockSee assets/README.md.template for a ready-to-fill skeleton with all sections pre-structured.
Annotate the output line token by token — users shouldn't have to guess what H:4 M:2 (6↑) means:
🔒 snyk │ deps H:4 M:2 (6↑) │ code H:2 M:3 │ test-project · 5m ago ⟳
^^^^ ^^^ ^^^ ^^^^ ^^^^ ^^^ ^^^ ^^^^^^^^^^^^^ ^^^^^^^^ ^
[A] [B] [C] [D] [E] [F] [G] [H] [I] [J][A] scan type label, [B][C] severity counts, [D] fixable, [E–G] same for second scan, [H] project name, [I] cache age, [J] background scan active
Done when: A developer unfamiliar with the project can install and use the statusline using the README alone.
Example 1: Building a security statusline from scratch
User says: "I want to see Snyk vulnerability counts in my Claude statusline."
Actions:
snyk test --json (SCA) and snyk code test --json (SAST)deps H:N M:N (N↑) and code H:N M:N, plus · Xs ago ⟳statusline.sh with two background scan functions and separate caches;
parse SCA with .vulnerabilities[] and SAST with .runs[0].results[] (SARIF)~/.claude/settings.json using the {type, command} object formatcat -vResult: A single status bar line showing live dep and code security status, refreshing every 5 minutes in the background, with zero impact on Claude's response time.
Example 2: Adding a second data source to an existing statusline
User says: "My statusline shows git branch — can I also add the last CI run status?"
Actions:
statusline.sh to understand its cache and output structuregh run list --limit 1 --json status,conclusion)CI_CACHE, CI_LOCK, CI_NOSCAN) following the existing patterntrigger_ci_bg() function using the same atomic-lock patternbuild_ci_segment() function; handle the case where gh is not authenticatedprintf output lineResult: The statusline now shows both git state and CI status on one line, each refreshing independently, without either scan blocking the other.
Example 3: Debugging a statusline stuck on "scanning..."
User says: "My statusline has been saying 'scanning...' for 20 minutes."
Actions:
ls ~/.cache/<tool>-statusline/*.lockrmdir ~/.cache/<tool>-statusline/*.locksnyk test --json 2>&1 | head -20cat ~/.cache/<tool>-statusline/*.errsnyk auth), no manifest in directory
(statusline should show "no manifest" not "scanning"), background process killedrm -rf ~/.cache/<tool>-statusline/echo '{}' | ./statusline.sh and watch for the "initializing..." → "scanning..." → result progressionResult: Root cause identified and resolved; statusline shows current data.
Error: Settings Error: statusLine: Expected object, but received string
Cause: settings.json has "statusLine": "/path/to/script" as a plain string.
Solution: Change it to the object format: "statusLine": {"type": "command", "command": "/path/to/script"}. Run the jq update command from Step 4.
Error: Statusline stuck on "scanning..." indefinitely.
Cause: A stale lock directory left by a crashed background scan process.
Solution: Delete the lock: rmdir ~/.cache/<tool>-statusline/*.lock. The next render will trigger a fresh scan.
Error: Statusline shows "initializing..." after every Claude restart.
Cause: The cache TTL has expired between sessions, or the cache directory is being
cleared on restart.
Solution: Increase CACHE_TTL (e.g. export TOOL_STATUSLINE_TTL=3600), or check
whether something is clearing ~/.cache on startup.
Error: Colors don't render — raw escape codes like ^[[38;2;... show in the bar.
Cause: The terminal or Claude Code version doesn't support RGB ANSI. Less likely:
escape codes are being double-escaped.
Solution: Verify codes are defined as $'...' literals (e.g. RED=$'\033[38;2;255;85;85m'),
not as regular strings with \033. Use cat -v to inspect raw output.
Error: Background scan never completes — cache file never appears.
Cause: The scan command is failing silently. The || true pattern suppresses the
exit code, so errors go undetected.
Solution: Run the scan command manually. Check the .err file in the cache dir.
Ensure exit code 3 ("no supported project") writes a .noscan sentinel instead of
being treated as a generic failure — otherwise the script retries every render.
46e15c0
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.