Skip to content

Instantly share code, notes, and snippets.

@sunapi386
Created February 7, 2026 22:45
Show Gist options
  • Select an option

  • Save sunapi386/56be9fc3eb5bc3b6024b52a6f3e53493 to your computer and use it in GitHub Desktop.

Select an option

Save sunapi386/56be9fc3eb5bc3b6024b52a6f3e53493 to your computer and use it in GitHub Desktop.
Claude Code Command Center for tmux — one-shot installer
#!/usr/bin/env bash
# Claude Code Command Center — one-shot installer
# Run: bash ~/.claude/command-center/install.sh
set -euo pipefail
log() { printf '\e[1;34m→\e[0m %s\n' "$1"; }
ok() { printf '\e[1;32m✓\e[0m %s\n' "$1"; }
err() { printf '\e[1;31m✗\e[0m %s\n' "$1" >&2; exit 1; }
# ── Preflight ────────────────────────────────────────────────────────────────
command -v jq >/dev/null || err "jq not found"
command -v flock >/dev/null || err "flock not found"
command -v tmux >/dev/null || err "tmux not found"
command -v fish >/dev/null || err "fish not found"
# ── Directories ──────────────────────────────────────────────────────────────
log "Creating directories"
mkdir -p "$HOME/.claude/hooks"
mkdir -p "$HOME/.claude/command-center/sessions"
# ── 1. Hook script ──────────────────────────────────────────────────────────
log "Writing hook script"
cat > "$HOME/.claude/hooks/command-center.sh" << 'HOOKEOF'
#!/usr/bin/env bash
# Claude Code Command Center — hook script
# Handles: SessionStart, SessionEnd, Notification, PreToolUse
# Writes per-session JSON state to ~/.claude/command-center/sessions/
set -euo pipefail
STATE_DIR="$HOME/.claude/command-center/sessions"
mkdir -p "$STATE_DIR"
INPUT=$(cat)
EVENT=$(echo "$INPUT" | jq -r '.event // empty')
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
[ -z "$EVENT" ] && exit 0
[ -z "$SESSION_ID" ] && exit 0
STATE_FILE="$STATE_DIR/${SESSION_ID}.json"
LOCK_FILE="$STATE_FILE.lock"
TMP_FILE="$STATE_FILE.tmp.$$"
NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
cleanup() { rm -f "$TMP_FILE"; }
trap cleanup EXIT
case "$EVENT" in
SessionStart)
CWD=$(echo "$INPUT" | jq -r '.cwd // empty')
PROJECT=$(basename "${CWD:-unknown}")
PANE="${TMUX_PANE:-}"
jq -n \
--arg sid "$SESSION_ID" \
--arg cwd "${CWD:-}" \
--arg project "$PROJECT" \
--arg started "$NOW" \
--arg activity "$NOW" \
--arg pane "$PANE" \
'{
session_id: $sid,
cwd: $cwd,
project: $project,
started_at: $started,
last_activity: $activity,
tmux_pane: $pane,
status: "active",
pending_question: null,
current_tool: null
}' > "$TMP_FILE" && mv "$TMP_FILE" "$STATE_FILE"
;;
SessionEnd)
rm -f "$STATE_FILE" "$LOCK_FILE"
;;
Notification)
(
flock -n 200 || exit 0
NOTIF_TYPE=$(echo "$INPUT" | jq -r '.notification.type // "notification"')
MESSAGE=$(echo "$INPUT" | jq -r '.notification.message // empty')
if [ -f "$STATE_FILE" ]; then
jq \
--arg activity "$NOW" \
--arg ntype "$NOTIF_TYPE" \
--arg msg "$MESSAGE" \
--arg asked "$NOW" \
'.status = "waiting"
| .last_activity = $activity
| .pending_question = {
type: $ntype,
message: $msg,
asked_at: $asked
}' "$STATE_FILE" > "$TMP_FILE" && mv "$TMP_FILE" "$STATE_FILE"
fi
) 200>"$LOCK_FILE"
;;
PreToolUse)
(
flock -n 200 || exit 0
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool.name // empty')
TOOL_DESC=$(echo "$INPUT" | jq -r '.tool.description // empty')
if [ -f "$STATE_FILE" ]; then
jq \
--arg activity "$NOW" \
--arg tname "$TOOL_NAME" \
--arg tdesc "$TOOL_DESC" \
'.status = "active"
| .last_activity = $activity
| .pending_question = null
| .current_tool = {
name: $tname,
description: $tdesc
}' "$STATE_FILE" > "$TMP_FILE" && mv "$TMP_FILE" "$STATE_FILE"
fi
) 200>"$LOCK_FILE"
;;
esac
HOOKEOF
chmod +x "$HOME/.claude/hooks/command-center.sh"
ok "Hook script"
# ── 2. Fish functions ────────────────────────────────────────────────────────
log "Writing fish functions"
FISH_FUNCS_DIR="$HOME/.config/fish/my_funcs"
mkdir -p "$FISH_FUNCS_DIR"
cat > "$FISH_FUNCS_DIR/claude-center.fish" << 'FISHEOF'
# Claude Code Command Center — fish functions
# Auto-loaded by config.fish via the my_funcs loop
function _cc_cleanup_stale
set -l sessions_dir "$HOME/.claude/command-center/sessions"
for f in $sessions_dir/*.json
test -f "$f"; or continue
set -l pane (jq -r '.tmux_pane // empty' "$f" 2>/dev/null)
if test -n "$pane"
if not tmux display-message -t "$pane" -p '' 2>/dev/null
rm -f "$f" "$f.lock"
end
end
end
end
function _cc_status_count
set -l sessions_dir "$HOME/.claude/command-center/sessions"
set -l waiting 0
set -l total 0
for f in $sessions_dir/*.json
test -f "$f"; or continue
set total (math $total + 1)
set -l status_val (jq -r '.status // empty' "$f" 2>/dev/null)
if test "$status_val" = waiting
set waiting (math $waiting + 1)
end
end
if test $total -eq 0
echo ""
else if test $waiting -gt 0
echo "#[fg=red,bold]CC:"$waiting"!#[default] "
else
echo "#[fg=green]CC:ok#[default] "
end
end
function _cc_render_session --argument-names file mode
set -l data (cat "$file" 2>/dev/null)
test -n "$data"; or return
set -l project (echo "$data" | jq -r '.project // "unknown"')
set -l status_val (echo "$data" | jq -r '.status // "active"')
set -l pane (echo "$data" | jq -r '.tmux_pane // ""')
set -l pending_msg (echo "$data" | jq -r '.pending_question.message // empty')
set -l pending_type (echo "$data" | jq -r '.pending_question.type // empty')
set -l tool_name (echo "$data" | jq -r '.current_tool.name // empty')
set -l tool_desc (echo "$data" | jq -r '.current_tool.description // empty')
set -l asked_at (echo "$data" | jq -r '.pending_question.asked_at // empty')
# Get tmux window:pane index for display
set -l pane_display ""
if test -n "$pane"
set pane_display (tmux display-message -t "$pane" -p '[#{window_index}:#{pane_index}]' 2>/dev/null; or echo "[$pane]")
end
# Calculate wait duration
set -l wait_str ""
if test -n "$asked_at"
set -l asked_epoch (date -d "$asked_at" +%s 2>/dev/null)
if test -n "$asked_epoch"
set -l now_epoch (date +%s)
set -l diff (math $now_epoch - $asked_epoch)
set -l mins (math "floor($diff / 60)")
set -l secs (math "$diff % 60")
set wait_str (printf "%dm%02ds" $mins $secs)
end
end
if test "$status_val" = waiting
set_color red; echo -n " ! "; set_color normal
set_color --bold; echo -n "$project"; set_color normal
echo " $pane_display"
set_color yellow
if test -n "$wait_str"
echo -n " Waiting $wait_str: "
else
echo -n " Waiting: "
end
if test -n "$pending_type" -a -n "$tool_name"
echo "Permission needed for $tool_name($tool_desc)"
else if test -n "$pending_msg"
set -l max_len 60
if test "$mode" = narrow
set max_len 30
end
if test (string length "$pending_msg") -gt $max_len
echo (string sub -l $max_len "$pending_msg")"..."
else
echo "$pending_msg"
end
else
echo "Needs attention"
end
set_color normal
else
set_color cyan; echo -n " > "; set_color normal
set_color --bold; echo -n "$project"; set_color normal
echo " $pane_display"
if test -n "$tool_name"
set_color brblack
if test -n "$tool_desc"
set -l max_len 60
if test "$mode" = narrow
set max_len 30
end
set -l desc "$tool_desc"
if test (string length "$desc") -gt $max_len
set desc (string sub -l $max_len "$desc")"..."
end
echo " $tool_name: $desc"
else
echo " $tool_name"
end
set_color normal
end
end
end
function claude_center --argument-names mode
test -n "$mode"; or set mode full
set -l sessions_dir "$HOME/.claude/command-center/sessions"
mkdir -p "$sessions_dir"
while true
clear
_cc_cleanup_stale
set -l waiting_files
set -l active_files
set -l total 0
for f in $sessions_dir/*.json
test -f "$f"; or continue
set total (math $total + 1)
set -l status_val (jq -r '.status // "active"' "$f" 2>/dev/null)
if test "$status_val" = waiting
set -a waiting_files "$f"
else
set -a active_files "$f"
end
end
# Sort waiting by asked_at (oldest first = most urgent)
if test (count $waiting_files) -gt 1
set waiting_files (for f in $waiting_files
set -l asked (jq -r '.pending_question.asked_at // "9999"' "$f" 2>/dev/null)
echo "$asked $f"
end | sort | string replace -r '^\S+ ' '')
end
# Header
if test "$mode" = narrow
set_color --bold brwhite
echo " COMMAND CENTER"
set_color normal
set_color brblack
echo " "(date +%H:%M:%S)" | q=quit"
set_color normal
else if test "$mode" = status-only
_cc_status_count
return
else
set_color --bold brwhite
echo " CLAUDE CODE COMMAND CENTER"
set_color normal
set_color brblack
echo " "(date +%H:%M:%S)" | Press q to quit"
set_color normal
end
echo ""
# Waiting section
if test (count $waiting_files) -gt 0
set_color --bold red
echo " NEEDS ATTENTION"
set_color brblack
if test "$mode" = narrow
echo " ──────────────────────────"
else
echo " ─────────────────────────────────────────"
end
set_color normal
for f in $waiting_files
_cc_render_session "$f" "$mode"
end
echo ""
end
# Active section
if test (count $active_files) -gt 0
set_color --bold cyan
echo " ACTIVE"
set_color brblack
if test "$mode" = narrow
echo " ──────────────────────────"
else
echo " ─────────────────────────────────────────"
end
set_color normal
for f in $active_files
_cc_render_session "$f" "$mode"
end
echo ""
end
if test $total -eq 0
set_color brblack
echo " No active Claude sessions"
set_color normal
echo ""
end
# Footer
set_color brblack
echo " "(count $waiting_files)" waiting | "(count $active_files)" active | $total total"
set_color normal
# Wait for input or timeout (2s refresh)
# fish read --timeout is in seconds
set -l key ""
if not read --nchars 1 --timeout 2 key 2>/dev/null
sleep 1.5
end
switch "$key"
case q Q
return
case j J
if test (count $waiting_files) -gt 0
set -l pane (jq -r '.tmux_pane // empty' "$waiting_files[1]" 2>/dev/null)
if test -n "$pane"
tmux select-pane -t "$pane" 2>/dev/null
tmux select-window -t "$pane" 2>/dev/null
return
end
end
end
end
end
function claude_center_sidebar
set -l existing (tmux list-panes -F '#{pane_id} #{pane_start_command}' 2>/dev/null | grep 'claude_center' | head -1 | string split ' ')[1]
if test -n "$existing"
tmux kill-pane -t "$existing" 2>/dev/null
else
tmux split-window -h -l 40 "fish -c 'claude_center narrow'"
end
end
function claude_center_window
set -l existing (tmux list-windows -F '#{window_id} #{window_name}' 2>/dev/null | grep 'CC-Dashboard' | head -1 | string split ' ')[1]
if test -n "$existing"
tmux kill-window -t "$existing" 2>/dev/null
else
tmux new-window -n 'CC-Dashboard' "fish -c 'claude_center full'"
end
end
function claude_center_popup
tmux display-popup -w 80% -h 70% -E "fish -c 'claude_center full'"
end
function claude_center_jump
set -l sessions_dir "$HOME/.claude/command-center/sessions"
_cc_cleanup_stale
set -l oldest_pane ""
set -l oldest_time "9999"
for f in $sessions_dir/*.json
test -f "$f"; or continue
set -l status_val (jq -r '.status // "active"' "$f" 2>/dev/null)
if test "$status_val" = waiting
set -l asked (jq -r '.pending_question.asked_at // "9999"' "$f" 2>/dev/null)
if test "$asked" \< "$oldest_time"
set oldest_time "$asked"
set oldest_pane (jq -r '.tmux_pane // empty' "$f" 2>/dev/null)
end
end
end
if test -n "$oldest_pane"
tmux select-window -t "$oldest_pane" 2>/dev/null
tmux select-pane -t "$oldest_pane" 2>/dev/null
tmux display-message "Jumped to waiting Claude session" 2>/dev/null
else
tmux display-message "No waiting Claude sessions" 2>/dev/null
end
end
FISHEOF
ok "Fish functions"
# ── 3. Settings hooks ────────────────────────────────────────────────────────
log "Updating ~/.claude/settings.json"
SETTINGS="$HOME/.claude/settings.json"
HOOK_CMD="bash \$HOME/.claude/hooks/command-center.sh"
HOOK_ENTRY='[{"matcher":"","hooks":[{"type":"command","command":"'"$HOOK_CMD"'","timeout":5}]}]'
if [ -f "$SETTINGS" ]; then
cp "$SETTINGS" "$SETTINGS.bak"
jq --argjson entry "$HOOK_ENTRY" '
.hooks //= {} |
.hooks.Notification = $entry |
.hooks.PreToolUse = $entry |
.hooks.SessionStart = $entry |
.hooks.SessionEnd = $entry
' "$SETTINGS" > "$SETTINGS.tmp" && mv "$SETTINGS.tmp" "$SETTINGS"
else
jq -n --argjson entry "$HOOK_ENTRY" '{
hooks: {
Notification: $entry,
PreToolUse: $entry,
SessionStart: $entry,
SessionEnd: $entry
}
}' > "$SETTINGS"
fi
ok "Settings hooks (backup at settings.json.bak)"
# ── 4. tmux keybindings ─────────────────────────────────────────────────────
log "Updating ~/.tmux.conf"
TMUX_CONF="$HOME/.tmux.conf"
MARKER="# Claude Code Command Center"
if [ -f "$TMUX_CONF" ] && grep -qF "$MARKER" "$TMUX_CONF"; then
# Already present — replace the block
sed -i "/^$MARKER$/,/^$/{ /^$MARKER$/!{ /^$/!d; }; }" "$TMUX_CONF"
sed -i "/^$MARKER$/a\\
bind-key C-c display-popup -w 80% -h 70% -E \"fish -c 'claude_center full'\"\\
bind-key M-c run-shell \"fish -c 'claude_center_sidebar'\"\\
bind-key C-w run-shell \"fish -c 'claude_center_window'\"\\
bind-key C-j run-shell \"fish -c 'claude_center_jump'\"\\
set -g status-right '#(fish -c \"_cc_status_count\" 2>/dev/null) %H:%M '\\
set -g status-interval 2" "$TMUX_CONF"
ok "tmux.conf (updated existing block)"
elif [ -f "$TMUX_CONF" ]; then
cp "$TMUX_CONF" "$TMUX_CONF.bak"
# Insert before TPM init block (comment + run line), or append
if grep -qF "# Initialize TMUX plugin manager" "$TMUX_CONF"; then
sed -i "/^# Initialize TMUX plugin manager/i\\
$MARKER\\
bind-key C-c display-popup -w 80% -h 70% -E \"fish -c 'claude_center full'\"\\
bind-key M-c run-shell \"fish -c 'claude_center_sidebar'\"\\
bind-key C-w run-shell \"fish -c 'claude_center_window'\"\\
bind-key C-j run-shell \"fish -c 'claude_center_jump'\"\\
set -g status-right '#(fish -c \"_cc_status_count\" 2>/dev/null) %H:%M '\\
set -g status-interval 2\\
" "$TMUX_CONF"
elif grep -qF "run '~/.tmux/plugins/tpm/tpm'" "$TMUX_CONF"; then
sed -i "/^run '~\/.tmux\/plugins\/tpm\/tpm'/i\\
$MARKER\\
bind-key C-c display-popup -w 80% -h 70% -E \"fish -c 'claude_center full'\"\\
bind-key M-c run-shell \"fish -c 'claude_center_sidebar'\"\\
bind-key C-w run-shell \"fish -c 'claude_center_window'\"\\
bind-key C-j run-shell \"fish -c 'claude_center_jump'\"\\
set -g status-right '#(fish -c \"_cc_status_count\" 2>/dev/null) %H:%M '\\
set -g status-interval 2\\
" "$TMUX_CONF"
else
cat >> "$TMUX_CONF" << 'TMUXEOF'
# Claude Code Command Center
bind-key C-c display-popup -w 80% -h 70% -E "fish -c 'claude_center full'"
bind-key M-c run-shell "fish -c 'claude_center_sidebar'"
bind-key C-w run-shell "fish -c 'claude_center_window'"
bind-key C-j run-shell "fish -c 'claude_center_jump'"
set -g status-right '#(fish -c "_cc_status_count" 2>/dev/null) %H:%M '
set -g status-interval 2
TMUXEOF
fi
ok "tmux.conf (backup at .tmux.conf.bak)"
else
err "~/.tmux.conf not found"
fi
# ── 5. Reload ────────────────────────────────────────────────────────────────
if [ -n "${TMUX:-}" ]; then
log "Reloading tmux config"
tmux source-file "$TMUX_CONF" 2>/dev/null && ok "tmux reloaded" || echo " (reload manually: tmux source-file ~/.tmux.conf)"
else
log "Not inside tmux — reload manually: tmux source-file ~/.tmux.conf"
fi
printf '\n\e[1;32mDone!\e[0m Command Center installed.\n'
printf ' Prefix+Ctrl-c popup Prefix+Alt-c sidebar\n'
printf ' Prefix+Ctrl-w window Prefix+Ctrl-j jump\n'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment