Implement AI-powered statusline with session tracking, plan detection, workspace emojis, and intelligent caching for Claude Code
/plugin marketplace add wildcard/shell-ergonomics-skills/plugin install wildcard-shell-ergonomics-skills@wildcard/shell-ergonomics-skillsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
scripts/statusline-wrapper.shThis skill implements a comprehensive statusline system for Claude Code featuring:
Main Statusline Wrapper (statusline-starship-wrapper.sh)
Session Naming Hook (session-naming.sh)
UserPromptSubmitPlan Tracking Hook (plan-tracking.sh)
SessionStart, UserPromptSubmitAssistant Output Sampling Hook (assistant-output-sampling.sh)
PostToolUse (after every tool execution)Shared Cache Cleanup (cache-cleanup.sh)
User Input โ UserPromptSubmit Hook โ session-naming.sh โ /tmp/claude-session-name-{session_id}.txt
โ plan-tracking.sh โ /tmp/claude-session-plan-{session_id}.txt
Tool Execution โ PostToolUse Hook โ assistant-output-sampling.sh โ /tmp/claude-assistant-summary-{session_id}.txt
Statusline Refresh โ statusline-starship-wrapper.sh โ Reads all caches โ Displays 3-line output
๐ฆ Sonnet 4.5 | [starship git status] | 63% ctx
Implementing advanced statusline ๐ง Edit, Write, Bash | abc-123-session-id
Advanced Statusline System ๐ plan-name-if-active
~/.claude/hooks/session-naming.sh#!/bin/bash
set -euo pipefail
# Read hook input
input=$(cat)
session_id=$(echo "$input" | jq -r '.session_id // ""')
user_prompt=$(echo "$input" | jq -r '.user_prompt // ""')
# Exit if no session ID or prompt
if [ -z "$session_id" ] || [ -z "$user_prompt" ]; then
exit 0
fi
# Filter out trivial inputs
if echo "$user_prompt" | grep -qiE '^(yes|no|ok|okay|sure|continue|try again|test it|fix it|run it|check|show|nope|yep|go ahead|proceed)\.?$'; then
exit 0
fi
# Also filter out very short inputs (< 10 chars)
if [ ${#user_prompt} -lt 10 ]; then
exit 0
fi
# Cache file
cache_file="/tmp/claude-session-name-${session_id}.txt"
timestamp_file="/tmp/claude-session-name-${session_id}.timestamp"
# If we already have a cached name, keep it (idle sessions should keep their name)
if [ -f "$cache_file" ]; then
exit 0
fi
# Generate session name using AI (with timeout)
schema='{"type":"object","properties":{"name":{"type":"string","maxLength":40}},"required":["name"]}'
prompt="Generate a concise session name (3-5 words, max 40 chars) for this task: ${user_prompt}"
ai_temp="/tmp/claude-session-name-ai-${session_id}-$$.json"
# Run with timeout (2 seconds)
(
echo "$prompt" | claude --model haiku -p --no-session-persistence --output-format json --json-schema "$schema" 2>/dev/null > "$ai_temp"
) &
claude_pid=$!
for i in {1..2}; do
if ! kill -0 $claude_pid 2>/dev/null; then
break
fi
sleep 1
done
kill $claude_pid >/dev/null 2>&1
wait $claude_pid >/dev/null 2>&1
# Extract name
session_name=""
if [ -f "$ai_temp" ] && [ -s "$ai_temp" ]; then
session_name=$(jq -r '.structured_output.name // empty' "$ai_temp" 2>/dev/null)
rm -f "$ai_temp"
fi
# Fallback: use first 40 chars of prompt
if [ -z "$session_name" ] || [ ${#session_name} -lt 5 ]; then
session_name=$(echo "$user_prompt" | head -c 40 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
fi
# Cache the name and timestamp
echo "$session_name" > "$cache_file"
date +%s > "$timestamp_file"
# Occasionally run shared cleanup (1% of the time to minimize overhead)
if [ $((RANDOM % 100)) -eq 0 ]; then
bash ~/.claude/hooks/cache-cleanup.sh &
fi
exit 0
~/.claude/hooks/plan-tracking.sh#!/bin/bash
set -euo pipefail
# Read hook input
input=$(cat)
session_id=$(echo "$input" | jq -r '.session_id // ""')
hook_event=$(echo "$input" | jq -r '.hook_event_name // ""')
if [ -z "$session_id" ]; then
exit 0
fi
# Plan tracking file
plan_map="/tmp/claude-plan-session-map.jsonl"
# On SessionStart or UserPromptSubmit, check for recent plans
if [ "$hook_event" = "SessionStart" ] || [ "$hook_event" = "UserPromptSubmit" ]; then
# Find most recent plan (within last 10 minutes)
recent_plan=$(find ~/.claude/plans -name "*.md" -mmin -10 -type f 2>/dev/null | head -1)
if [ -n "$recent_plan" ]; then
plan_name=$(basename "$recent_plan" .md)
# Record mapping
echo "{\"session_id\":\"${session_id}\",\"plan\":\"${plan_name}\",\"timestamp\":$(date +%s)}" >> "$plan_map"
# Cache current session's plan
echo "$plan_name" > "/tmp/claude-session-plan-${session_id}.txt"
fi
fi
# Keep plan map under control (keep only last 1000 entries)
if [ -f "$plan_map" ]; then
tail -1000 "$plan_map" > "${plan_map}.tmp" && mv "${plan_map}.tmp" "$plan_map"
fi
# Occasionally run shared cleanup (1% of the time)
if [ $((RANDOM % 100)) -eq 0 ]; then
bash ~/.claude/hooks/cache-cleanup.sh &
fi
exit 0
~/.claude/hooks/assistant-output-sampling.sh#!/bin/bash
set -euo pipefail
# Read hook input
input=$(cat)
session_id=$(echo "$input" | jq -r '.session_id // ""')
hook_event=$(echo "$input" | jq -r '.hook_event_name // ""')
if [ -z "$session_id" ]; then
exit 0
fi
# Only run on PostToolUse
if [ "$hook_event" != "PostToolUse" ]; then
exit 0
fi
# Extract tool information
tool_name=$(echo "$input" | jq -r '.tool_name // ""')
tool_input=$(echo "$input" | jq -r '.tool_input // {}')
# Skip empty or system tools
if [ -z "$tool_name" ] || [ "$tool_name" = "null" ]; then
exit 0
fi
# Cache file for assistant actions
actions_cache="/tmp/claude-assistant-actions-${session_id}.jsonl"
# Record action with timestamp
action_entry=$(jq -n \
--arg tool "$tool_name" \
--arg timestamp "$(date +%s)" \
--argjson input "$tool_input" \
'{tool: $tool, timestamp: $timestamp, input: $input}')
echo "$action_entry" >> "$actions_cache"
# Keep only last 50 actions
if [ -f "$actions_cache" ]; then
tail -50 "$actions_cache" > "${actions_cache}.tmp" && mv "${actions_cache}.tmp" "$actions_cache"
fi
# Generate summary of recent actions (last 10)
recent_actions=$(tail -10 "$actions_cache" 2>/dev/null | jq -r '.tool' | sort | uniq -c | sort -rn | head -3 | awk '{print $2}' | paste -sd ', ' -)
if [ -n "$recent_actions" ]; then
echo "$recent_actions" > "/tmp/claude-assistant-summary-${session_id}.txt"
fi
# Occasionally run shared cleanup (1% of the time)
if [ $((RANDOM % 100)) -eq 0 ]; then
bash ~/.claude/hooks/cache-cleanup.sh &
fi
exit 0
~/.claude/hooks/cache-cleanup.sh#!/bin/bash
# Shared cache cleanup utility - only cleans when total cache > 100MB
# Keeps recent files (last 7 days) even when cleaning
set -euo pipefail
# Calculate total cache size
cache_size=$(du -sm /tmp/claude-* 2>/dev/null | awk '{sum+=$1} END {print sum+0}')
# Only cleanup if > 100MB
if [ "$cache_size" -lt 100 ]; then
exit 0
fi
# Cleanup old files (> 7 days) when cache is large
find /tmp -name "claude-session-summary-*.txt" -mtime +7 -delete 2>/dev/null
find /tmp -name "claude-session-summary-*.timestamp" -mtime +7 -delete 2>/dev/null
find /tmp -name "claude-session-summary-*.msgcount" -mtime +7 -delete 2>/dev/null
find /tmp -name "claude-session-name-*.txt" -mtime +7 -delete 2>/dev/null
find /tmp -name "claude-session-name-*.timestamp" -mtime +7 -delete 2>/dev/null
find /tmp -name "claude-session-plan-*.txt" -mtime +7 -delete 2>/dev/null
find /tmp -name "claude-assistant-actions-*.jsonl" -mtime +7 -delete 2>/dev/null
find /tmp -name "claude-assistant-summary-*.txt" -mtime +7 -delete 2>/dev/null
# Also cleanup temporary AI summary files (always clean these up if > 1 hour old)
find /tmp -name "claude-ai-summary-*.json" -mmin +60 -delete 2>/dev/null
exit 0
~/.claude/statusline-starship-wrapper.sh#!/bin/bash
# Read JSON input from stdin
input=$(cat)
# Extract current directory from JSON
cwd=$(echo "$input" | jq -r '.workspace.current_dir')
# Assign workspace emoji based on path
get_workspace_emoji() {
local path="$1"
local basename=$(basename "$path")
# Semantic mapping for common project types
case "$basename" in
*caro*) echo "๐ฆ" ;; # Rust crab
*rust*) echo "๐ฆ" ;;
*node*|*npm*|*react*|*next*) echo "๐ฆ" ;;
*python*|*py*) echo "๐" ;;
*go*|*golang*) echo "๐น" ;;
*java*) echo "โ" ;;
*docker*) echo "๐ณ" ;;
*web*|*site*) echo "๐" ;;
*api*) echo "๐" ;;
*db*|*database*) echo "๐๏ธ" ;;
*docs*|*documentation*) echo "๐" ;;
*test*) echo "๐งช" ;;
*)
# Hash-based consistent emoji for unknown paths
local emojis=("๐ผ" "๐" "๐ ๏ธ" "โ๏ธ" "๐ง" "๐" "๐ฏ" "๐" "๐ก" "๐ฌ")
local hash=$(echo -n "$path" | cksum | cut -d' ' -f1)
local index=$((hash % ${#emojis[@]}))
echo "${emojis[$index]}"
;;
esac
}
workspace_emoji=$(get_workspace_emoji "$cwd")
# Extract model display name
model_name=$(echo "$input" | jq -r '.model.display_name')
# Extract session ID
session_id=$(echo "$input" | jq -r '.session_id')
# Read session name from cache (generated by hook)
session_name_cache="/tmp/claude-session-name-${session_id}.txt"
if [ -f "$session_name_cache" ]; then
session_name=$(cat "$session_name_cache" 2>/dev/null || echo "")
fi
# Fallback if no cached name
if [ -z "$session_name" ]; then
session_name=$(echo "$input" | jq -r '.session_name // "Unnamed Session"')
fi
# Read plan info from cache (generated by hook)
plan_cache="/tmp/claude-session-plan-${session_id}.txt"
plan_indicator=""
if [ -f "$plan_cache" ]; then
plan_name=$(cat "$plan_cache" 2>/dev/null || echo "")
if [ -n "$plan_name" ]; then
plan_indicator=" ๐ $plan_name"
fi
fi
# Read assistant action summary from cache (generated by PostToolUse hook)
assistant_summary_cache="/tmp/claude-assistant-summary-${session_id}.txt"
assistant_actions=""
if [ -f "$assistant_summary_cache" ]; then
actions=$(cat "$assistant_summary_cache" 2>/dev/null || echo "")
if [ -n "$actions" ]; then
assistant_actions=" ๐ง $actions"
fi
fi
# Calculate context window percentage
usage=$(echo "$input" | jq '.context_window.current_usage')
if [ "$usage" != "null" ]; then
current=$(echo "$usage" | jq '.input_tokens + .cache_creation_input_tokens + .cache_read_input_tokens')
size=$(echo "$input" | jq '.context_window.context_window_size')
context_pct=$((current * 100 / size))
context_display=" | ${context_pct}% ctx"
else
context_display=""
fi
# Set up environment variables that Starship expects
export PWD="$cwd"
export STARSHIP_SHELL="bash"
# Change to the directory so git commands work correctly
cd "$cwd" 2>/dev/null || cd "$HOME"
# Run starship with the prompt command and strip the \[\] escape sequences
starship_output=$(starship prompt 2>/dev/null | sed 's/\\\[//g; s/\\\]//g' | tr -d '\n')
# Generate session summary (line 2)
summary_cache="/tmp/claude-session-summary-${session_id}.txt"
summary_timestamp="/tmp/claude-session-summary-${session_id}.timestamp"
summary_msg_count="/tmp/claude-session-summary-${session_id}.msgcount"
# Check if we need to regenerate the summary
# Regenerate ONLY if:
# 1. Cache doesn't exist, OR
# 2. New messages exist since last check
# Idle sessions keep their cached summary indefinitely
should_regenerate=false
history_file="$HOME/.claude/history.jsonl"
# Get current message count
current_msg_count=0
if [ -f "$history_file" ]; then
current_msg_count=$(grep -c "\"sessionId\":\"${session_id}\"" "$history_file" 2>/dev/null || echo "0")
fi
if [ ! -f "$summary_cache" ]; then
should_regenerate=true
else
# Check if new messages exist
last_msg_count=$(cat "$summary_msg_count" 2>/dev/null || echo "0")
if [ "$current_msg_count" -gt "$last_msg_count" ]; then
should_regenerate=true
fi
fi
if [ "$should_regenerate" = true ]; then
# Read from history.jsonl instead of transcript
history_file="$HOME/.claude/history.jsonl"
if [ -f "$history_file" ]; then
# Process line-by-line to avoid jq -s parse errors
first_user_msg=$(grep "\"sessionId\":\"${session_id}\"" "$history_file" | head -1 | jq -r '.display // ""')
recent_user_msgs=$(grep "\"sessionId\":\"${session_id}\"" "$history_file" | tail -7 | jq -r '.display' | paste -sd ' ' -)
user_msg_count=$(grep -c "\"sessionId\":\"${session_id}\"" "$history_file")
else
first_user_msg=""
recent_user_msgs=""
user_msg_count=0
fi
# PRIMARY METHOD: AI summary with structured output
# Use ALL user messages (no keyword filtering - let AI determine what's meaningful)
all_user_msgs=$(grep "\"sessionId\":\"${session_id}\"" "$history_file" | jq -r '.display')
# Get first + last 5 user messages for context
first_user_msgs=$(echo "$all_user_msgs" | head -5 | paste -sd '. ' -)
last_user_msgs=$(echo "$all_user_msgs" | tail -5 | paste -sd '. ' -)
# Use structured JSON output with examples
schema='{"type":"object","properties":{"task":{"type":"string","maxLength":60}},"required":["task"]}'
prompt="Summarize this development session in max 60 chars based on what the user asked for and worked on.
User's requests (first 5): $first_user_msgs
User's requests (last 5): $last_user_msgs
Examples: Configuring statusline, Adding AI summaries, Fixing build errors, Refactoring auth module"
# Call Claude with structured output using temp file for timeout handling
ai_temp="/tmp/claude-ai-summary-${session_id}-$$.json"
(
echo "$prompt" | claude --model haiku -p --no-session-persistence --output-format json --json-schema "$schema" 2>/dev/null > "$ai_temp"
) &
claude_pid=$!
# Wait up to 3 seconds for completion (statusline needs to be fast)
for i in {1..3}; do
if ! kill -0 $claude_pid 2>/dev/null; then
break
fi
sleep 1
done
# Kill if still running (suppress all output)
kill $claude_pid >/dev/null 2>&1
wait $claude_pid >/dev/null 2>&1
# Extract summary from temp file
ai_summary=""
if [ -f "$ai_temp" ] && [ -s "$ai_temp" ]; then
ai_summary=$(jq -r '.structured_output.task // empty' "$ai_temp" 2>/dev/null)
rm -f "$ai_temp"
fi
# Check if AI summary worked and is meaningful
if [ -n "$ai_summary" ] && [ ${#ai_summary} -gt 10 ] && ! echo "$ai_summary" | grep -qiE "^(Ready|Awaiting|Clarify|Assist)"; then
summary="$ai_summary"
else
# FALLBACK: Use first + last message
first_line=$(echo "$first_user_msg" | head -n 1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | cut -c 1-47)
last_user_msg=$(grep "\"sessionId\":\"${session_id}\"" "$history_file" | tail -1 | jq -r '.display // ""' | head -n 1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | cut -c 1-47)
if [ "$user_msg_count" -le 1 ] || [ "$first_line" = "$last_user_msg" ]; then
summary=$(echo "$first_line" | cut -c 1-100)
else
summary="${first_line} โ ${last_user_msg}"
fi
fi
# Clean up summary
summary=$(echo "$summary" | tr -s ' ' | tr -d '\n\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
# Final fallback if still empty
if [ -z "$summary" ]; then
summary="Active session"
fi
# Cache the summary and message count
echo "$summary" > "$summary_cache"
date +%s > "$summary_timestamp"
echo "$current_msg_count" > "$summary_msg_count"
else
# Use cached summary
summary=$(cat "$summary_cache" 2>/dev/null || echo "Active session")
fi
# Occasionally run shared cleanup (1% of the time to minimize overhead)
if [ $((RANDOM % 100)) -eq 0 ]; then
bash ~/.claude/hooks/cache-cleanup.sh &
fi
# Combine workspace emoji, model name, starship output, and context percentage (line 1)
# Add session summary with session ID and assistant actions (line 2)
# Add session name with plan indicator (line 3)
printf "%s %s | %s%s\n%s%s | %s\n%s%s" "$workspace_emoji" "$model_name" "$starship_output" "$context_display" "$summary" "$assistant_actions" "$session_id" "$session_name" "$plan_indicator"
</details>
chmod +x ~/.claude/hooks/*.sh
chmod +x ~/.claude/statusline-starship-wrapper.sh
Add to ~/.claude/settings.json:
{
"statusLine": {
"type": "command",
"command": "bash ~/.claude/statusline-starship-wrapper.sh"
},
"hooks": {
"UserPromptSubmit": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/session-naming.sh",
"timeout": 5
},
{
"type": "command",
"command": "bash ~/.claude/hooks/plan-tracking.sh",
"timeout": 2
}
]
}
],
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/assistant-output-sampling.sh",
"timeout": 1
}
]
}
],
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/plan-tracking.sh",
"timeout": 2
}
]
}
]
}
}
Claude Code passes JSON to the statusline command via stdin:
{
"session_id": "abc-123-def",
"session_name": "My Session",
"workspace": {
"current_dir": "/path/to/project"
},
"model": {
"display_name": "Sonnet 4.5"
},
"context_window": {
"current_usage": {
"input_tokens": 50000,
"output_tokens": 10000,
"cache_creation_input_tokens": 5000,
"cache_read_input_tokens": 30000
},
"context_window_size": 200000
}
}
Hooks receive different JSON based on event type:
UserPromptSubmit:
{
"session_id": "abc-123",
"user_prompt": "The text the user typed",
"hook_event_name": "UserPromptSubmit",
...
}
PostToolUse:
{
"session_id": "abc-123",
"tool_name": "Edit",
"tool_input": {"file_path": "/path", ...},
"tool_result": {...},
"hook_event_name": "PostToolUse",
...
}
~/.claude/history.jsonl - User messages only~/.claude/projects/-{project-path}/{session-id}.jsonl~/.claude/plans/{plan-name}.md~/.claude/settings.jsonStarship outputs bash readline escapes (\[ and \]) that must be stripped:
starship_output=$(starship prompt 2>/dev/null | sed 's/\\\[//g; s/\\\]//g' | tr -d '\n')
Why: Statusline doesn't use readline, these escapes cause display issues.
Starship requires these environment variables:
export PWD="$cwd"
export STARSHIP_SHELL="bash"
cd "$cwd" 2>/dev/null || cd "$HOME"
Why: Starship uses PWD for directory display and git commands need correct working directory.
| File Pattern | Purpose | Lifetime |
|---|---|---|
claude-session-name-*.txt | Auto-generated session names | Until 100MB + 7 days |
claude-session-summary-*.txt | AI-generated summaries | Until 100MB + 7 days |
claude-session-plan-*.txt | Active plan detection | Until 100MB + 7 days |
claude-assistant-summary-*.txt | Tool usage summaries | Until 100MB + 7 days |
claude-assistant-actions-*.jsonl | Full action logs | Until 100MB + 7 days |
claude-ai-summary-*.json | Temp AI responses | 1 hour (always) |
claude-plan-session-map.jsonl | Plan correlation history | Kept at 1000 entries |
Session Name:
AI Summary:
Plan Detection:
Assistant Actions:
chmod +x ~/.claude/statusline-starship-wrapper.shecho '{"session_id":"test",...}' | bash ~/.claude/statusline-starship-wrapper.shwhich starshipwhich jqwhich claudeecho "test" | claude --model haiku -p --no-session-persistencels -lh /tmp/claude-session-summary-*echo '{"session_id":"test","user_prompt":"hello"}' | bash ~/.claude/hooks/session-naming.shdu -sm /tmp/claude-* 2>/dev/null | awk '{sum+=$1} END {print sum " MB"}'bash ~/.claude/hooks/cache-cleanup.sh-mtime +7 to -mtime +3)If you see \[ or \] in statusline:
's/\\\[//g; s/\\\]//g' (three backslashes)echo "\\[test\\]" | sed 's/\\\[//g; s/\\\]//g'Edit get_workspace_emoji() function in statusline script:
case "$basename" in
*myproject*) echo "๐ฏ" ;;
*backend*) echo "โ๏ธ" ;;
# Add more...
esac
Modify schema in statusline script:
schema='{"type":"object","properties":{"task":{"type":"string","maxLength":80}},"required":["task"]}'
Edit cache-cleanup.sh:
# Change 100MB threshold to 50MB
if [ "$cache_size" -lt 50 ]; then
Edit cache-cleanup.sh:
# Change 7 days to 3 days
find /tmp -name "claude-session-summary-*.txt" -mtime +3 -delete
/tmp (world-readable on most systems)--no-session-persistence (no data retention)set -euo pipefail for safetyThis skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.