Automatically rename the session from the plan's H1 heading when Plan Mode ends, and automatically repair the name after compaction via the sidecar mechanism.
How It Works
Three shell scripts work together:
plan-rename-hook.sh(PreToolUseExitPlanMode): extract the plan H1 heading and write it to the sidecar backup file (pending: true)。does not write directly to JSONL — avoids flickering when the user rejects the planplan-rename-guard.sh(Stop hook): after each Claude response, check:- Phase 1 — Pending confirmation: when the sidecar is pending, scan the JSONL to confirm whether ExitPlanMode was accepted. If rejected, skip; only if accepted, write custom-title
- Phase 2 — Fallback repair: when the sidecar is confirmed but the JSONL lacks custom-title, re-inject it (a safety net; normally handled by the compact hook)
plan-rename-compact.sh(SessionStartcompact): triggered specifically after compaction:- Read the confirmed sidecar → scan the tail of the JSONL → if custom-title is missing, re-inject it
- Also clean up sidecars with mtime > 30 days (skip
_pending_*)
Flowchart
Plan Mode exit
→ ExitPlanMode hook (PreToolUse)
→ 擷取 H1 標題(去除 "Plan:" 前綴,截斷 80 字元)
→ 寫入 sidecar(pending: true)
→ 不寫 JSONL(等 Stop guard 確認)
使用者 accept/reject plan
→ Claude 回應後 Stop hook 觸發
→ bash 快速檢查:有 pending sidecar 或 confirmed sidecar 嗎?
→ 都沒有 → exit 0(~5ms)
→ 有 pending → Python 分析 JSONL
→ rejected → 跳過
→ accepted → 寫入 custom-title,sidecar 設為 confirmed
→ 有 confirmed 但無 custom-title → 兜底重新注入
Compaction 發生
→ SessionStart compact hook 觸發
→ 有 confirmed sidecar?
→ 沒有 → exit 0(~5ms)
→ 有 → 掃描 JSONL 尾部
→ 有 custom-title → exit 0
→ 沒有 → 重新注入 custom-title
→ 清理 30 天以上的 sidecar
Hook Registration
After installation, you need tomanuallyin ~/.claude/settings.json 's hooks block add the following three settings:
{
"hooks": {
"PreToolUse": [
{
"matcher": "ExitPlanMode",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/skills/plan-rename/plan-rename-hook.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/skills/plan-rename/plan-rename-guard.sh"
}
]
}
],
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/skills/plan-rename/plan-rename-compact.sh"
}
]
}
]
}
}
Note:
npx skills addonly installs files; it will not automatically modifysettings.json。
Upgrading from v1 to v2
Main improvements in v2:
- Removed the
jqdependency — guard.sh now usesgrep+cutto parse JSON - Added
plan-rename-compact.sh— a dedicated SessionStart compact hook that triggers precisely after compaction instead of running on every response - Improved Stop hook performance — Phase 2 (compaction repair) is downgraded to a fallback; 99% of cases are handled by the compact hook
Upgrade steps:
- Update
plan-rename-guard.shand addplan-rename-compact.sh(updating the files is sufficient) - In
settings.jsonadd theSessionStartcompact hook (see Hook Registration above) - Confirm that
jqis no longer needed (it can be removed without affecting other tools)
Hook stdin Schemas
PreToolUse ExitPlanMode:
{
"session_id": "uuid-string",
"transcript_path": "/Users/.../.claude/projects/<slug>/<session_id>.jsonl",
"hook_event_name": "PreToolUse",
"tool_name": "ExitPlanMode",
"tool_input": { "plan": "# Plan Title\n...", "allowedPrompts": [] }
}
Stop hook:
{
"session_id": "uuid-string",
"transcript_path": "/Users/.../.claude/projects/<slug>/<session_id>.jsonl"
}
SessionStart compact:
{
"session_id": "uuid-string",
"transcript_path": "/Users/.../.claude/projects/<slug>/<session_id>.jsonl",
"hook_event_name": "SessionStart",
"source": "compact",
"cwd": "/Users/...",
"permission_mode": "default",
"model": "claude-sonnet-4-6"
}
JSONL custom-title Format
Uses the same format as the Claude Code /rename command:
{"type":"custom-title","customTitle":"<title>","sessionId":"<session_id>"}
Before you use it
Pros
- Plan Mode exit automatically names the session — no manual
/rename - Automatic repair after compaction — compact hook triggers precisely + Stop hook as a fallback
- No API token consumption — all hooks are local shell commands
- No external dependencies — no
jqneeded (v2 uses grep + cut)
Cons / Limitations
- Manual configuration required (
settings.jsonhook(npx skills addonly installs files and does not change settings) - The Stop hook runs on every response (but exits in ~5ms when there is no sidecar)
/clearwhen creating a new session the title will not carry forward
Potential issues
- Sidecar accumulation:
~/.claude/session-titles/will keep accumulating, about ~100 bytes each. The compact hook automatically cleans up sidecars older than 30 days - Compaction behavior: compaction rewrites the JSONL and discards non-message entries; this is an observation/inference rather than official documentation. Claude Code updates may change the behavior
/clearlimitation:/clearwhen creating a new session the title will not carry forward
Token / IO cost
| Item | Cost |
|---|---|
| API Token | Zero overhead — pure local shell commands |
| Stop hook (guard) | No sidecar: grep fast exit (~5ms); with pending: Python JSONL tail scan (~100-150ms) |
| SessionStart compact hook | Triggered only on compaction; no sidecar: bash exit (~5ms); with sidecar: Python scan (~80-120ms) |
| PreToolUse hook | Triggered only on ExitPlanMode, Python runs once (~50-80ms) |
| Disk I/O | guard reads sidecar (~100 bytes) + last 50 lines of JSONL; hook writes one JSONL line + one sidecar file (atomic temp+rename) |
| Summary | Sessions not using plan rename are virtually unaffected (~5ms/response); compaction repair is handled on demand by the compact hook |
Script stability risk
- JSONL
custom-titleThe format was obtained through observation of/renamebehavior via reverse engineering, not an official API — Claude Code updates may change the format - Hook stdin JSON schema(
session_id、transcript_pathand other fields) likewise have no official guarantee ExitPlanModeonly triggersPreToolUse(does not triggerPostToolUse) is an observation resultSessionStartofcompactmatcher is a Claude Code v2.1.75+ feature- Suggestion: After a major Claude Code version update, run integration tests to confirm the hooks still work properly
System requirements
python3bash
Security
- Session ID validation:
[a-zA-Z0-9_-]{8,128}regex - Path containment: transcript_path must be within
~/.claude/inside - Control character cleanup: title removes Unicode category C characters
- Sidecar permissions: directory 700, file 600
- Atomic write: temp file + rename to prevent partial write
Common Pitfalls
- Compaction drops custom-title: JSONL is managed by Claude Code; non-message entries may be cleared by compaction. Must be backed up using a sidecar
- Stop hook bash pre-check: Using grep to parse JSON and skip Python is the key performance optimization
- PostToolUse cannot intercept Plan Mode: Must use PreToolUse ExitPlanMode
- PreToolUse triggers before user approval: Therefore the hook only writes the sidecar (pending), and the Stop guard writes to JSONL only after confirming accept. Avoids title flickering caused by rejected plans
- Field naming: hook stdin uses
session_id(snake_case), notsessionId(camelCase) - Path source: Use the
transcript_pathprovided by stdin, do not manually concatenate - compact hook stdout: Keep blank to avoid triggering bug #15174