diff --git a/hooks/README.md b/hooks/README.md index c7884e3..e46fac2 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,24 @@ 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 | +| `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. + ### 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..13285e5 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,24 @@ Hooks 分為兩個層級: | `.rb` | `rubocop -a` | 內建 | | `.java` | `google-java-format` | 內建 | +### status-hook.sh + +在三個事件寫入 session 狀態至 `~/.claude/sessions/.status`。 + +**格式:** ` ` + +| 狀態 | 事件 | 說明 | +|------|------|------| +| `working` | UserPromptSubmit | 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。 + ### notify-on-stop.sh 在 `Stop` 事件觸發。當 Claude 完成回應時發送通知。僅在 session 持續超過 **30 秒**時觸發(避免快速回覆的噪音)。 diff --git a/hooks/install.sh b/hooks/install.sh index 9608456..ad49edb 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 working"}]}]) + 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