Skip to content

feat(hooks): cache-handoff Stop hook wakes idle claude for a hand-off - #526

Open
lroolle wants to merge 1 commit into
mainfrom
feat/cache-handoff-525
Open

feat(hooks): cache-handoff Stop hook wakes idle claude for a hand-off#526
lroolle wants to merge 1 commit into
mainfrom
feat/cache-handoff-525

Conversation

@lroolle

@lroolle lroolle commented Jul 29, 2026

Copy link
Copy Markdown
Member

Summary

Idle claude sessions burn the 1h prompt cache with nothing written down. The built-in away-summary needs terminal blur and skips past 0.9x TTL — headless/container sessions (deva's whole mode) never get one.

This ships hooks/cache-handoff.sh, an opt-in Stop hook using the asyncRewake exit-2 wake primitive (verified in claude 2.1.220): 50min after the last Stop, if the session is still idle, it wakes the model to write a hand-off file while the cache re-read is still nearly free.

Never interrupts running work — three gates:

  • transcript-growth abort (a turn is in flight)
  • per-session pidfile supersede (async hooks are not deduplicated; every Stop spawns a sleeper)
  • donefile dedup (fires once per transcript position)

New hooks/ dir follows the skills/ precedent: repo asset, copy-in install, zero wiring. Deliberately not wired into deva.sh — ~/.claude is host-mounted; same non-mutation call as the statusline non-redirect.

Verification

  • scripts/test-cache-handoff.sh: 6 hermetic gate cases (scratch state dir, no claude), added to CI
  • shellcheck clean (deva.sh, agents/, scripts/, hooks/*, severity=error)
  • deva.sh --help / --version unaffected (no launcher changes)
  • docs section on existing page, no mkdocs nav change

Risk

Rides internal unflagged claude settings fields (asyncRewake, rewakeMessage, rewakeSummary). A future claude release can drop them; failure mode is the hook never wakes anything. Caveat documented in hooks/README.md.

Closes #525

🤖 Generated with Claude Code

- new hooks/ dir: opt-in copy-in Claude Code assets, skills/-style
- cache-handoff.sh sleeps 50min after Stop, exits 2 (asyncRewake) to
  wake the still-idle session while the 1h prompt cache is warm and
  have it write a hand-off file
- three gates: transcript-growth abort, per-session pidfile supersede,
  once-per-transcript-position dedup; fails closed if claude drops the
  internal asyncRewake fields
- not wired into deva.sh: ~/.claude is host-mounted, deva keeps its
  hands off it; install is documented copy-in per workspace
- scripts/test-cache-handoff.sh: 6 hermetic gate cases, wired into CI
- docs: advanced-usage section + hooks/README.md

Close #525

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 29, 2026 06:05
@claude-yolo

claude-yolo Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error after 1s —— View job


I'll analyze this and get back to you.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c0547e6f1a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread hooks/cache-handoff.sh
# process behind.
if [[ -f "$pidfile" ]]; then
old=$(cat "$pidfile" 2>/dev/null)
[[ -n "$old" && "$old" != "$$" ]] && kill "$old" 2>/dev/null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Terminate the superseded hook's sleep child

When another Stop arrives during the 50-minute delay, old identifies the Bash hook process, but sleep is a separate child. Killing Bash leaves that child reparented to PID 1 and sleeping for the full duration; repeated turns therefore accumulate exactly the sleeper processes this gate is intended to eliminate. Use a trap or process-group-aware cancellation that also terminates the child.

Useful? React with 👍 / 👎.

Comment thread hooks/cache-handoff.sh
Comment on lines +50 to +52
if [[ -f "$pidfile" ]]; then
old=$(cat "$pidfile" 2>/dev/null)
[[ -n "$old" && "$old" != "$$" ]] && kill "$old" 2>/dev/null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify stale PID ownership before sending a signal

In a persistent container, this pidfile remains after the hook fires or exits through a later gate, so a subsequent Stop can encounter it after that PID has been recycled. In that case this unconditional kill terminates an unrelated process owned by the same user, potentially including Claude or an active tool. Remove the pidfile on exit and/or record and validate process identity before signaling it.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in Claude Code Stop hook to “rewake” an idle session before the ~1h prompt cache expires, prompting the model to write a hand-off file while context re-read is still cheap. This fits deva’s container-first workflow by shipping the hook as a copy-in repo asset (no automatic mutation of host ~/.claude), plus hermetic gate tests and CI wiring.

Changes:

  • Add hooks/cache-handoff.sh async Stop hook with idle-gating (transcript-growth abort, per-session sleeper supersede, once-per-position dedup).
  • Add hermetic gate tests (scripts/test-cache-handoff.sh) and run them in CI.
  • Document installation/behavior in hooks/README.md and docs/advanced-usage.md, and note the feature in CHANGELOG.md / DEV-LOGS.md.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
scripts/test-cache-handoff.sh New hermetic smoke tests covering the hook’s gate/supersede/dedup behavior.
hooks/README.md New documentation for hook installation, config wiring, knobs, and caveats.
hooks/cache-handoff.sh New Stop hook implementation that sleeps then exits 2 to rewake only when still idle.
docs/advanced-usage.md Adds an “Cache Handoff Hook” section pointing to the hook and docs.
DEV-LOGS.md Adds a dev log entry documenting rationale, behavior, and risk caveats.
CHANGELOG.md Adds an Unreleased “Added” entry for the new hook feature.
.github/workflows/ci.yml Runs the new cache-handoff smoke test in CI.
Comments suppressed due to low confidence (1)

hooks/README.md:67

  • “container-local, nothing persists or leaks to the host” is only accurate when Claude Code is running inside a container (as deva typically does). If someone uses this hook outside deva, the state dir is host-local. Consider wording this as “local to the environment running Claude Code” to keep it accurate in both cases.
State is per-session pid + done files; container-local, nothing
persists or leaks to the host.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread hooks/cache-handoff.sh
Comment on lines +34 to +36
mkdir -p "$STATE_DIR" || exit 0
pidfile="$STATE_DIR/$session.pid"
donefile="$STATE_DIR/$session.done"
Comment thread hooks/cache-handoff.sh
Comment on lines +50 to +53
if [[ -f "$pidfile" ]]; then
old=$(cat "$pidfile" 2>/dev/null)
[[ -n "$old" && "$old" != "$$" ]] && kill "$old" 2>/dev/null
fi
Comment thread hooks/cache-handoff.sh
Comment on lines +21 to +33
FIRE_AFTER="${CC_HANDOFF_AFTER_SEC:-3000}"
STATE_DIR="${CC_HANDOFF_STATE_DIR:-${TMPDIR:-/tmp}/cc-cache-handoff}"

input=$(cat)
read -r active session transcript <<<"$(
printf '%s' "$input" | jq -r '[(.stop_hook_active // false), .session_id, (.transcript_path // "")] | @tsv'
)"

# The rewake itself marks the next turn stop-hook-active. Never chain
# off our own wake.
[[ "$active" == "true" ]] && exit 0
[[ -z "$session" || "$session" == "null" ]] && exit 0

Comment thread hooks/README.md

```
CC_HANDOFF_AFTER_SEC seconds idle before firing (default 3000)
CC_HANDOFF_STATE_DIR pid/done state dir (default $TMPDIR/cc-cache-handoff)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hooks: cache-handoff Stop hook - wake idle claude before 1h prompt cache expires

2 participants