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
20 changes: 20 additions & 0 deletions hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand All @@ -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
Expand Down Expand Up @@ -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/<pid>.status`.

**Format:** `<status> <epoch>`

| 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).
Expand Down
20 changes: 20 additions & 0 deletions hooks/README.zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand All @@ -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` — 攔截存取憑證檔案
Expand Down Expand Up @@ -93,6 +95,24 @@ Hooks 分為兩個層級:
| `.rb` | `rubocop -a` | 內建 |
| `.java` | `google-java-format` | 內建 |

### status-hook.sh

在三個事件寫入 session 狀態至 `~/.claude/sessions/<pid>.status`。

**格式:** `<status> <epoch>`

| 狀態 | 事件 | 說明 |
|------|------|------|
| `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 秒**時觸發(避免快速回覆的噪音)。
Expand Down
14 changes: 14 additions & 0 deletions hooks/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 .
Expand Down
51 changes: 51 additions & 0 deletions hooks/status-hook.sh
Original file line number Diff line number Diff line change
@@ -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: "<status> <epoch>" in ~/.claude/sessions/<claude_pid>.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 <working|idle|waiting>}"
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