Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion hosts/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const codex: HostConfig = {
generation: {
generateMetadata: true,
metadataFormat: 'openai.yaml',
skipSkills: ['codex'], // Codex skill is a Claude wrapper around codex exec
skipSkills: ['codex', 'statusline-setup'], // codex: Claude wrapper; statusline-setup: Claude Code-only (~/.claude/settings.json)
},

pathRewrites: [
Expand Down
839 changes: 839 additions & 0 deletions statusline-setup/SKILL.md

Large diffs are not rendered by default.

82 changes: 82 additions & 0 deletions statusline-setup/SKILL.md.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
name: statusline-setup
preamble-tier: 2
version: 1.0.0
description: |
Install the Claude Code statusline. Shows current directory, git branch, active model,
context window %, 5-hour rate limit usage, and Spotify now playing (macOS).
Use when setting up Claude Code for the first time, adding the statusline to a new
machine, or when asked to "set up statusline", "install statusline", or
"show now playing in Claude". Claude Code only β€” uses the statusLine settings.json
field not available on other hosts. (gstack)
allowed-tools:
- Read
- Write
- Edit
- Bash
triggers:
- set up statusline
- install statusline
- show spotify in claude
- configure status bar
- set up status bar
---

{{PREAMBLE}}

# /statusline-setup β€” Claude Code Status Bar

> **Claude Code only.** This skill uses the `statusLine` field in `~/.claude/settings.json`,
> which is not available on Codex, Cursor, Kiro, or other hosts. On unsupported hosts,
> skip this skill and let the user know.

This skill installs a shell-based Claude Code statusline that shows at a glance:

- πŸ“ Current directory
- 🌿 Git branch (only when inside a git repo)
- πŸ€– Active model name
- πŸ“Š Context window usage % (green β†’ yellow β†’ red at 80%)
- πŸ’° 5-hour rate limit usage %
- β™« Spotify now playing (macOS only β€” silently omitted on Linux/Windows)

---

## Step 1 β€” Install the script

Read the script at `${CLAUDE_SKILL_DIR}/statusline.sh` and write it verbatim to
`~/.claude/statusline.sh`.

Then make it executable:

```bash
chmod +x ~/.claude/statusline.sh
```

## Step 2 β€” Wire it into settings.json

Read `~/.claude/settings.json`. If the file does not exist, create it as `{}`.

Add or update the `statusLine` field, preserving all existing fields:

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

Use Edit (not Write) if the file already has content, so existing config is preserved.

## Step 3 β€” Confirm

Tell the user:

- `~/.claude/statusline.sh` was installed
- The statusline will appear on the next Claude Code interaction (no restart needed)
- **macOS:** Spotify integration requires granting Terminal Automation permission for
Spotify in System Settings β†’ Privacy & Security β†’ Automation. Without it, the β™«
section is silently omitted.
- **Linux / Windows:** The Spotify section is always omitted (`osascript` is macOS-only).
All other fields (directory, branch, model, context %, rate limit %) work on all platforms.
94 changes: 94 additions & 0 deletions statusline-setup/statusline.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/bin/sh
# Claude Code statusline script
# Shows: directory, git branch, model, context usage, rate limit usage, LOC, Spotify now playing
#
# Install:
# 1. Copy this file to ~/.claude/statusline.sh
# 2. Copy loc.sh to ~/.claude/loc.sh
# 3. chmod +x ~/.claude/statusline.sh ~/.claude/loc.sh
# 4. Add to ~/.claude/settings.json:
# "statusLine": { "type": "command", "command": "sh ~/.claude/statusline.sh" },
# "hooks": {
# "UserPromptSubmit": [{
# "hooks": [{ "type": "command", "async": true,
# "command": "cwd=$(jq -r '.cwd // empty'); [ -n \"$cwd\" ] && (cd \"$cwd\" && sh ~/.claude/loc.sh > /tmp/claude-loc 2>/dev/null) || true" }]
# }]
# }

input=$(cat)
cwd=$(echo "$input" | jq -r '.cwd')
model=$(echo "$input" | jq -r '.model.display_name')
used=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
rate_five=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')

dir=$(basename "$cwd")

CYAN='\033[0;36m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
MAGENTA='\033[0;35m'
BLUE='\033[0;34m'
RESET='\033[0m'

git_branch=$(git -C "$cwd" --no-optional-locks symbolic-ref --short HEAD 2>/dev/null)

output=""

# Directory
output="${output}$(printf "${CYAN}πŸ“ %s${RESET}" "$dir")"

# Git branch (only when inside a git repo)
if [ -n "$git_branch" ]; then
output="${output}$(printf " ${GREEN}🌿 %s${RESET}" "$git_branch")"
fi

# Model
output="${output}$(printf " ${MAGENTA}πŸ€– %s${RESET}" "$model")"

# Context usage
if [ -n "$used" ]; then
if [ "$(printf '%.0f' "$used")" -ge 80 ]; then
ctx_emoji="πŸ”΄"
ctx_color="$YELLOW"
elif [ "$(printf '%.0f' "$used")" -ge 50 ]; then
ctx_emoji="πŸ“Š"
ctx_color="$BLUE"
else
ctx_emoji="πŸ“Š"
ctx_color="$BLUE"
fi
output="${output}$(printf " ${ctx_color}${ctx_emoji} %.0f%% context window${RESET}" "$used")"
fi

# Rate limit usage (5-hour session)
if [ -n "$rate_five" ]; then
output="${output}$(printf " ${YELLOW}πŸ’° %.0f%% usage${RESET}" "$rate_five")"
fi

# LOC β€” updated by UserPromptSubmit hook, read from cache (non-blocking)
if [ -f /tmp/claude-loc ]; then
loc=$(cat /tmp/claude-loc)
if [ -n "$loc" ]; then
output="${output}$(printf " ${CYAN}πŸ“ %s${RESET}" "$loc")"
fi
fi

# Spotify now playing (macOS only β€” requires Automation permission for Spotify)
spotify=$(osascript 2>/dev/null <<'EOF'
if application "Spotify" is running then
tell application "Spotify"
if player state is playing then
set t to name of current track
set a to artist of current track
return t & " – " & a
end if
end tell
end if
return ""
EOF
)
if [ -n "$spotify" ]; then
output="${output}$(printf " ${GREEN}β™« %s${RESET}" "$spotify")"
fi

printf "%b" "$output"
1 change: 1 addition & 0 deletions test/gen-skill-docs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1507,6 +1507,7 @@ describe('Codex generation (--host codex)', () => {
for (const entry of fs.readdirSync(ROOT, { withFileTypes: true })) {
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
if (entry.name === 'codex') continue; // /codex is excluded from Codex output
if (entry.name === 'statusline-setup') continue; // Claude Code-only skill (~/.claude/settings.json)
if (!fs.existsSync(path.join(ROOT, entry.name, 'SKILL.md.tmpl'))) continue;
const codexName = entry.name.startsWith('gstack-') ? entry.name : `gstack-${entry.name}`;
if (isSymlinkLoop(codexName)) continue;
Expand Down
1 change: 1 addition & 0 deletions test/skill-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1463,6 +1463,7 @@ describe('Codex skill validation', () => {
for (const entry of fs.readdirSync(ROOT, { withFileTypes: true })) {
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
if (entry.name === 'codex') continue; // Claude-only skill
if (entry.name === 'statusline-setup') continue; // Claude Code-only skill (~/.claude/settings.json)
if (fs.existsSync(path.join(ROOT, entry.name, 'SKILL.md.tmpl'))) {
skills.push(entry.name);
}
Expand Down