From fbf7d0370b61476183f28120d57b2a56239e111e Mon Sep 17 00:00:00 2001 From: MuLinForest <5525477+MuLinForest@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:25:57 +0800 Subject: [PATCH 1/2] feat(hooks): add status-hook.sh for real-time session status tracking Implement the status-hook slot that was already planned in install.sh and notify-on-stop.sh but never provided. - Add hooks/status-hook.sh: - Writes " " to ~/.claude/sessions/.status - Supports working (UserPromptSubmit), idle (Stop), waiting (PermissionRequest) - On "idle", preserves the epoch from the last "working" state so that notify-on-stop.sh can compute elapsed working time accurately - Walks up the process tree to find the claude PID (hooks run as direct children of claude, so this resolves in 1-2 iterations) - Update hooks/install.sh: - Install status-hook for UserPromptSubmit, Stop, and PermissionRequest in the recommended section (before notify-on-stop, which depends on it) - Step 7 ordering already enforces status-hook first in the Stop array - Update hooks/README.md and hooks/README.zh-TW.md: - Add status-hook.sh to quick reference table and layered defaults - Add detailed section explaining the .status file format and idle epoch-preservation behavior Co-Authored-By: Claude Code --- hooks/README.md | 18 +++++++++++++++ hooks/README.zh-TW.md | 18 +++++++++++++++ hooks/install.sh | 14 ++++++++++++ hooks/status-hook.sh | 51 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+) create mode 100755 hooks/status-hook.sh diff --git a/hooks/README.md b/hooks/README.md index c7884e3..92e63d2 100644 --- a/hooks/README.md +++ b/hooks/README.md @@ -11,6 +11,7 @@ Ready-to-use hook scripts for automating Claude Code workflows. Hooks integrate | `safety-guard.sh` | PreToolUse | Block dangerous commands (rm -rf /, force push, DROP TABLE) | | `sensitive-files.sh` | PreToolUse | Block access to .env, credentials, *.key files | | `auto-format.sh` | PostToolUse | Auto-format files after edit (prettier/black/gofmt/clang-format) | +| `status-hook.sh` | UserPromptSubmit / Stop / PermissionRequest | Write session status to `.status` file for dashboard and notify-on-stop | | `notify-on-stop.sh` | Stop | Desktop/tmux notification when Claude finishes (30s threshold) | | `context-alert.sh` | Stop | Warn when context usage exceeds 80% or 95% | | `usage-logger.sh` | Session | Log session usage to `~/.claude/hooks/usage.jsonl` | @@ -33,6 +34,7 @@ bash hooks/uninstall.sh Hooks are split into two tiers: **Recommended ON (enabled by default):** +- `status-hook.sh` — writes real-time session status for dashboard and notify-on-stop - `notify-on-stop.sh` — desktop/tmux notification when Claude finishes - `safety-guard.sh` — blocks destructive commands before they run - `sensitive-files.sh` — blocks access to credential files @@ -93,6 +95,22 @@ Fires on `PostToolUse` for file edit tool calls. Detects the file type and runs | `.rb` | `rubocop -a` | built-in | | `.java` | `google-java-format` | built-in | +### status-hook.sh + +Tracks the real-time status of each Claude session by writing a `.status` file to `~/.claude/sessions/.status`. + +**Format:** ` ` + +| Status | Event | Meaning | +|--------|-------|---------| +| `working` | UserPromptSubmit | Claude is processing a user message | +| `waiting` | PermissionRequest | Claude is waiting for tool permission | +| `idle` | Stop | Claude has finished responding | + +When writing `idle`, the hook **preserves the epoch from the last `working` state** rather than using the current time. This lets `notify-on-stop.sh` calculate elapsed working time accurately. + +The `.status` file is also read by the dashboard (`statusline/dashboard.sh`) to show real-time status without polling the session JSON. + ### notify-on-stop.sh Fires on `Stop` events. Sends a notification when Claude finishes a response. Only fires if the session has been active for more than **30 seconds** (avoids noise for quick replies). diff --git a/hooks/README.zh-TW.md b/hooks/README.zh-TW.md index 24e6b50..51f6afe 100644 --- a/hooks/README.zh-TW.md +++ b/hooks/README.zh-TW.md @@ -11,6 +11,7 @@ | `safety-guard.sh` | PreToolUse | 攔截危險指令(rm -rf /、force push、DROP TABLE)| | `sensitive-files.sh` | PreToolUse | 攔截存取 .env、credentials、*.key 等敏感檔案 | | `auto-format.sh` | PostToolUse | 編輯後自動格式化(prettier/black/gofmt/clang-format)| +| `status-hook.sh` | UserPromptSubmit / Stop / PermissionRequest | 寫入 `.status` 檔供 dashboard 和 notify-on-stop 即時讀取 | | `notify-on-stop.sh` | Stop | Claude 完成時桌面/tmux 通知(30 秒門檻)| | `context-alert.sh` | Stop | Context 使用超過 80% 或 95% 時警告 | | `usage-logger.sh` | Session | 記錄 session 使用量至 `~/.claude/hooks/usage.jsonl` | @@ -33,6 +34,7 @@ bash hooks/uninstall.sh Hooks 分為兩個層級: **建議開啟(預設啟用):** +- `status-hook.sh` — 即時寫入 session 狀態,供 dashboard 和 notify-on-stop 使用 - `notify-on-stop.sh` — Claude 完成時桌面/tmux 通知 - `safety-guard.sh` — 在執行前攔截破壞性指令 - `sensitive-files.sh` — 攔截存取憑證檔案 @@ -93,6 +95,22 @@ Hooks 分為兩個層級: | `.rb` | `rubocop -a` | 內建 | | `.java` | `google-java-format` | 內建 | +### status-hook.sh + +在三個事件寫入 session 狀態至 `~/.claude/sessions/.status`。 + +**格式:** ` ` + +| 狀態 | 事件 | 說明 | +|------|------|------| +| `working` | UserPromptSubmit | Claude 正在處理使用者訊息 | +| `waiting` | PermissionRequest | Claude 等待工具權限確認 | +| `idle` | Stop | Claude 完成回應 | + +寫入 `idle` 時,hook 會**保留上次 `working` 的 epoch**,讓 `notify-on-stop.sh` 能準確計算實際工作時間。 + +`.status` 檔也供 dashboard(`statusline/dashboard.sh`)即時顯示狀態,無需輪詢 session JSON。 + ### notify-on-stop.sh 在 `Stop` 事件觸發。當 Claude 完成回應時發送通知。僅在 session 持續超過 **30 秒**時觸發(避免快速回覆的噪音)。 diff --git a/hooks/install.sh b/hooks/install.sh index 9608456..72b6121 100755 --- a/hooks/install.sh +++ b/hooks/install.sh @@ -165,6 +165,20 @@ if [ "$_install_recommended" = "1" ]; then else .hooks.PreToolUse = ((.hooks.PreToolUse // []) + [{"matcher":"Read|Edit|Write","hooks":[{"type":"command","command":"sh ~/.claude/hooks/sensitive-files.sh"}]}]) end' + # status-hook (UserPromptSubmit + Stop + PermissionRequest) + # Must be installed before notify-on-stop; Stop ordering is enforced in Step 7. + # notify-on-stop depends on the .status file written by status-hook. + _jq_filter="$_jq_filter"' + | if ([(.hooks.UserPromptSubmit // [])[] | .hooks[]? | .command // ""] | any(test("hooks/status-hook"))) then . + else .hooks.UserPromptSubmit = ((.hooks.UserPromptSubmit // []) + [{"hooks":[{"type":"command","command":"sh ~/.claude/hooks/status-hook.sh working"}]}]) + end + | if ([(.hooks.Stop // [])[] | .hooks[]? | .command // ""] | any(test("hooks/status-hook"))) then . + else .hooks.Stop = ((.hooks.Stop // []) + [{"hooks":[{"type":"command","command":"sh ~/.claude/hooks/status-hook.sh idle"}]}]) + end + | if ([(.hooks.PermissionRequest // [])[] | .hooks[]? | .command // ""] | any(test("hooks/status-hook"))) then . + else .hooks.PermissionRequest = ((.hooks.PermissionRequest // []) + [{"hooks":[{"type":"command","command":"sh ~/.claude/hooks/status-hook.sh waiting"}]}]) + end' + # notify-on-stop (Stop) _jq_filter="$_jq_filter"' | if ([(.hooks.Stop // [])[] | .hooks[]? | .command // ""] | any(test("hooks/notify-on-stop"))) then . diff --git a/hooks/status-hook.sh b/hooks/status-hook.sh new file mode 100755 index 0000000..97583dc --- /dev/null +++ b/hooks/status-hook.sh @@ -0,0 +1,51 @@ +#!/bin/sh +# Claude Code hook: track session status in a .status file. +# +# Events (configure in settings.json): +# UserPromptSubmit → sh ~/.claude/hooks/status-hook.sh working +# Stop → sh ~/.claude/hooks/status-hook.sh idle +# PermissionRequest → sh ~/.claude/hooks/status-hook.sh waiting +# +# Output format: " " in ~/.claude/sessions/.status +# +# For "idle", the epoch written is the one saved when "working" was set — +# preserving the start of the working period so notify-on-stop.sh can +# compute elapsed time accurately. + +STATUS="${1:?Usage: status-hook.sh }" +SESSIONS_DIR="$HOME/.claude/sessions" + +# Find the Claude PID. Hooks run as direct children of claude, so $PPID is +# usually the claude PID itself. Walk up a few levels to be safe. +_claude_pid="" +_check="$PPID" +for _i in 1 2 3 4; do + _comm=$(ps -o comm= -p "$_check" 2>/dev/null | tr -d ' ') + if [ "$_comm" = "claude" ]; then + _claude_pid="$_check" + break + fi + _check=$(ps -o ppid= -p "$_check" 2>/dev/null | tr -d ' ') + [ -z "$_check" ] || [ "$_check" = "0" ] || [ "$_check" = "1" ] && break +done + +[ -z "$_claude_pid" ] && exit 0 + +_statusfile="$SESSIONS_DIR/${_claude_pid}.status" +_epoch=$(date +%s) + +if [ "$STATUS" = "idle" ]; then + # Preserve the epoch recorded when "working" was written so that + # notify-on-stop.sh can compute how long the task actually took. + _working_epoch="" + if [ -f "$_statusfile" ]; then + _prev_status="" _prev_epoch="" + read -r _prev_status _prev_epoch < "$_statusfile" 2>/dev/null || true + if [ "$_prev_status" = "working" ] && [ -n "$_prev_epoch" ]; then + _working_epoch="$_prev_epoch" + fi + fi + _epoch="${_working_epoch:-$_epoch}" +fi + +printf '%s %s\n' "$STATUS" "$_epoch" > "$_statusfile" 2>/dev/null || true From 9f0c6f9f6db33f1bc9160e93983108aef758859a Mon Sep 17 00:00:00 2001 From: MuLinForest <5525477+MuLinForest@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:50:49 +0800 Subject: [PATCH 2/2] fix(hooks): use working instead of waiting for PermissionRequest PermissionRequest fires on every tool call (including auto-approved ones), so writing "waiting" caused the dashboard to show WAITING throughout the entire working period rather than just when the user is genuinely blocking. Map PermissionRequest to "working" so the status stays WORKING for the full response cycle (thinking + tool execution + reply). WAITING remains available for future use cases that require true user-blocking distinction. Update README docs accordingly. Co-Authored-By: Claude Code --- hooks/README.md | 4 +++- hooks/README.zh-TW.md | 4 +++- hooks/install.sh | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/hooks/README.md b/hooks/README.md index 92e63d2..e46fac2 100644 --- a/hooks/README.md +++ b/hooks/README.md @@ -104,9 +104,11 @@ Tracks the real-time status of each Claude session by writing a `.status` file t | Status | Event | Meaning | |--------|-------|---------| | `working` | UserPromptSubmit | Claude is processing a user message | -| `waiting` | PermissionRequest | Claude is waiting for tool permission | +| `working` | PermissionRequest | Claude is executing a tool | | `idle` | Stop | Claude has finished responding | +Both `UserPromptSubmit` and `PermissionRequest` write `working`, so the status stays WORKING throughout the entire response (thinking + tool execution + reply). Only `Stop` transitions to `idle`. + When writing `idle`, the hook **preserves the epoch from the last `working` state** rather than using the current time. This lets `notify-on-stop.sh` calculate elapsed working time accurately. The `.status` file is also read by the dashboard (`statusline/dashboard.sh`) to show real-time status without polling the session JSON. diff --git a/hooks/README.zh-TW.md b/hooks/README.zh-TW.md index 51f6afe..13285e5 100644 --- a/hooks/README.zh-TW.md +++ b/hooks/README.zh-TW.md @@ -104,9 +104,11 @@ Hooks 分為兩個層級: | 狀態 | 事件 | 說明 | |------|------|------| | `working` | UserPromptSubmit | Claude 正在處理使用者訊息 | -| `waiting` | PermissionRequest | Claude 等待工具權限確認 | +| `working` | PermissionRequest | Claude 正在執行工具 | | `idle` | Stop | Claude 完成回應 | +`UserPromptSubmit` 和 `PermissionRequest` 都寫入 `working`,所以整個回覆期間(思考 + 工具執行 + 回覆)狀態都保持 WORKING,只有 `Stop` 才轉為 `idle`。 + 寫入 `idle` 時,hook 會**保留上次 `working` 的 epoch**,讓 `notify-on-stop.sh` 能準確計算實際工作時間。 `.status` 檔也供 dashboard(`statusline/dashboard.sh`)即時顯示狀態,無需輪詢 session JSON。 diff --git a/hooks/install.sh b/hooks/install.sh index 72b6121..ad49edb 100755 --- a/hooks/install.sh +++ b/hooks/install.sh @@ -176,7 +176,7 @@ if [ "$_install_recommended" = "1" ]; then else .hooks.Stop = ((.hooks.Stop // []) + [{"hooks":[{"type":"command","command":"sh ~/.claude/hooks/status-hook.sh idle"}]}]) end | if ([(.hooks.PermissionRequest // [])[] | .hooks[]? | .command // ""] | any(test("hooks/status-hook"))) then . - else .hooks.PermissionRequest = ((.hooks.PermissionRequest // []) + [{"hooks":[{"type":"command","command":"sh ~/.claude/hooks/status-hook.sh waiting"}]}]) + else .hooks.PermissionRequest = ((.hooks.PermissionRequest // []) + [{"hooks":[{"type":"command","command":"sh ~/.claude/hooks/status-hook.sh working"}]}]) end' # notify-on-stop (Stop)