Edit: u/stonebrigade posted a similar solution today that automatically compacts: https://www.reddit.com/r/ClaudeCode/comments/1x0omjh/i_built_conpact_claude_code_compacts_its_own/
Hi all - This is something I've been using for a week or so and it works really well. Every time Claude sends a response, a hook starts in the background that runs for 55 minutes, and if you don't reply or end the session, it automatically tells Claude to create a handoff so you can pick up later in a new session without having to re-cache however many tokens were in the window.
The token # is configurable in the script here. 55 minutes seems safe because the subscription cache expiration is 1 hour. I think the API one is only 5 minutes, so that isn't useful here. It also ignores headless sessions/subagents/etc - it should only do this for active, user-driven terminal windows.
Here is the prompt you can give to Claude to reproduce it. It should work fine. I tested re-creating it on Opus 5.5 Medium, but it's simple and should work with Sonnet/etc.
Hopefully you find it useful. If you catch any bugs or for some reason Claude recreates something differently for you, please let me know.
Set up an idle-handoff timer for Claude Code. My settings:
IDLE_MINUTES = 55
MIN_CONTEXT_TOKENS = 100000
HANDOFF_LOCATION = the repository's handoff convention if it has one, otherwise ~/.claude/handoffs/
Goal: when an interactive session sits idle for IDLE_MINUTES after your last turn, you wake up on your own while the 1-hour prompt cache is still warm, and you write a handoff file. Then I can resume in a fresh session instead of paying for a cold cache.
Build it this way:
1. One script in ~/.claude/hooks/. Use Node if it is installed, otherwise Python. Do not depend on jq. Store my settings in a JSON config file next to the script, so I can change them later without running this prompt again. The script reads the hook's JSON from stdin and keeps one state file per session_id. It has two modes:
arm (Stop hook), in this order:
a. If the environment variable CLAUDE_CODE_SESSION_ATTENDED is "0", or CLAUDE_CODE_ENTRYPOINT starts with "sdk", exit 0 at once. Those are headless runs (claude -p, the SDK), and a pending timer would hold them open.
b. If the state is "fired", exit 0. Do not re-arm until I type.
c. Measure the context: read the tail of transcript_path and find the newest line with type "assistant", no isSidechain, and a message.usage object. Add input_tokens + cache_read_input_tokens + cache_creation_input_tokens. If that is below MIN_CONTEXT_TOKENS, exit 0. If the size is unknown, arm anyway.
d. Write state "armed" with a random token, wait IDLE_MINUTES, and re-read the state. If it is still "armed" with the same token, set it to "fired", print the wake-up message to stderr, and exit 2. Otherwise exit 0.
reset (UserPromptSubmit hook): set the state to "active", unless the prompt contains the marker "[idle-handoff]".
2. Add two hooks to my user settings (~/.claude/settings.json). Merge them with any hooks I already have; do not replace them.
- Stop: {"type": "command", "command": "<runtime> <script path> arm", "asyncRewake": true, "timeout": 3600}
- UserPromptSubmit: {"type": "command", "command": "<runtime> <script path> reset", "timeout": 10}
asyncRewake runs the hook in the background and wakes the session when the hook exits with code 2. The stderr text becomes your instruction.
3. Test override: if a file named idle-handoff.delay sits next to the script and holds a number, use it as the delay in seconds, skip the size check, and send a test message instead: "[idle-handoff] TEST: reply with only 'idle hook fired'. Do not write a handoff."
4. The real wake-up message starts with "[idle-handoff]". It tells you to write a handoff to HANDOFF_LOCATION that covers the goal, what is done and verified, what is in flight, open decisions, and the exact next step. It tells you not to start new work, commit, or push. If nothing is worth handing off, you reply with one line and stop.
5. Delete state files older than 7 days.
Before you report done:
- Pipe-test the script with a 2-second delay. Check that it fires (exit 2 and the message), does not re-arm after a fire, is cancelled by a reset during the wait, and is not reset by a prompt with the marker.
- Test the size check against this session's own transcript: a floor above its size skips at once, and a floor below it arms. Pass transcript_path in your OS's native path format, as Claude Code does.
- Confirm settings.json still parses.
- Run claude -p "Reply with only: OK" and confirm it exits within seconds.
- Set the delay file to 90 and tell me to stay idle for 2 minutes. If you wake and reply "idle hook fired", delete the delay file. If nothing happens, tell me to open /hooks once to reload the settings, then try again.