Skip to content

[FEATURE] UserIdle hook event for proactive state persistence after N minutes of inactivity #58895

@csmarshall

Description

@csmarshall

Preflight Checklist

  • I have searched existing requests and this feature hasn't been requested yet
  • This is a single feature request (not multiple features)

Problem Statement

Claude Code sessions can run for hours when used as a project-management collaborator (multi-step refactors, doc batches, long iterative work). The assistant builds up a synthesized mental model of "where we are in the work" that's denser than the raw transcript.

claude --continue and claude --resume <id> handle the same-machine, same-session-lineage continuation case well: the new invocation reloads the JSONL transcript and resumes conversation. For crash recovery and "I closed the terminal and want to keep going" the existing flags work.

What's not addressed by --resume:

  1. Cross-machine continuity. The transcript lives at ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl — local to one host. If I work from a workstation Tuesday and switch to a laptop Friday, --resume doesn't follow. A project-side state file (committed or synced) does.

  2. Fresh sessions that aren't a --resume. Opening claude Monday morning on a project I last touched Friday, I usually want a fresh session — not a multi-day-old transcript replay. But I DO want the assistant to know what was last decided / in-flight. Today the only mechanism is a manually-maintained CLAUDE.md or project doc; an auto-checkpointed state file would be denser and current.

  3. Context-window cost of resume. --continue reloads the full transcript. On long sessions that's tens of thousands of tokens burned before the user has typed anything. A 1-2k-token curated state summary in a project-side file is materially cheaper to load.

  4. Survives session-history pruning. Per the Claude Code data-usage docs, commercial accounts retain session transcripts for 30 days by default. After that, --resume may not work even if the user knows the session ID. Project-side state files don't have this constraint.

The pattern in (1)-(4) is forward-looking state preservation that survives boundaries --resume can't bridge: machines, time windows, fresh-session starts. The existing hook surface (PreToolUse/PostToolUse, SessionStart/SessionEnd/Stop, UserPromptSubmit) fires on activity-events, not on inactivity. A hook that wants to write durable curated state has no natural trigger for "user has been quiet for a while; this is a good time to snapshot."

Proposed Solution

A new hook event UserIdle that fires after the user has been inactive (no prompt submission, no tool call) for a configurable threshold. Settings shape mirrors the existing hook structure:

{
  "hooks": {
    "UserIdle": [{
      "matcher": "*",
      "threshold": "5m",
      "hooks": [{
        "type": "command",
        "command": "/path/to/checkpoint-state.sh"
      }]
    }]
  }
}

Behavior:

  • Fires once when idle duration crosses the threshold. Does not re-fire while still idle.
  • Resets on any activity (new user prompt, new tool call). After activity, the timer starts fresh; another full idle period elapses before the next fire.
  • The threshold field accepts duration strings (5m, 30s, 1h) and is per-hook-group so multiple groups with different thresholds can coexist (e.g., quick-checkpoint at 5m, full-save at 30m).
  • The hook subprocess receives the standard hook stdin JSON payload plus an idle_duration_seconds field naming how long the user has been idle.

The minimum useful shape is single-fire-then-reset. Recurring fires within a single idle period, time-of-day predicates, multi-event compounding, etc. are out of scope for this request.

Alternative Solutions

Three workarounds I've tried or considered, none of which substitute for the proposed event:

1. Stop hook (hooks reference). Catches clean session-end via /exit or normal shutdown. Doesn't fire on: terminal close without exit, OS sleep, network disconnect on a resumable session, or the user simply walking away. The Stop hook is the closest existing primitive; it covers a different (smaller) failure surface than UserIdle would.

2. /loop <interval> <prompt> dynamic-loop pattern. The assistant can self-schedule a re-invocation every N minutes via the loop skill. Works as a poor-person's timer, but requires explicit per-session setup ("/loop 10m run a save-state check") and produces noise when nothing has changed since the last fire. It's also assistant-driven rather than client-driven, so a wedged or crashed session can't run it.

3. Aggressive in-session event-based persistence. The assistant saves working state proactively after every commit, every subagent return, every meaningful decision. Codified as a project rule in collaborator instructions. Helps in the "user is actively working but session crashes" case; doesn't help in the "user walked away" case — that's the gap UserIdle would close.

None of these three substitute for the proposed event. Stop addresses clean shutdown; /loop addresses periodic-with-noise; aggressive saving addresses crashes during activity. UserIdle would address inactivity-without-shutdown specifically.

Priority

Medium - Would be very helpful

Feature Category

Configuration and settings

Use Case Example

Concrete scenario (forward-looking, no crash):

  1. Tuesday morning I open Claude Code on a project, start a project-management session. The assistant reads project-side state files (the project's CLAUDE.md plus a maintainer-maintained session-state.md) and resumes where last week left off.

  2. Over four hours we land 8 commits across several docs and one new ADR. The assistant builds up a running mental model of "where we are" — which items are reviewed, which decisions are still open, which work is blocked on me.

  3. I close the laptop at lunch, head out, don't return Tuesday.

  4. Friday I open Claude Code on a different machine (synced repo, same project). I COULD ssh back to Tuesday's machine and claude --resume <id>, but: (a) that machine isn't on right now, (b) the four-hour transcript would burn my context-window budget on a fresh session before I've typed anything, and (c) I just want a dense summary to orient on — not a transcript replay.

If a UserIdle hook had fired after 10 minutes of idle Tuesday at lunch, the assistant would have written a curated state file to the project repo via a hook script. Friday's fresh session reads that file in seconds and is immediately oriented on what was in-flight.

--resume covers same-machine, same-session continuation. It doesn't cover the cross-machine handoff, multi-day gap, or fresh-session-without-resume cases — which are the default startup mode for most users. The UserIdle event would let project-side tooling fill those gaps without the user having to remember to invoke anything.

Additional Context

The maury project (https://github.com/csmarshall/maury/tree/devel) is one downstream tool building on this pattern; its CLAUDE.local.md "Aggressive session-state persistence" rule codifies the workaround discipline this feature would obviate. The pattern isn't maury-specific — any project using Claude Code as a long-running collaborator with persistent project-side state hits the same gap.

Related open issue: #44789 ("Auto-compact sessions after idle timeout") asks for a specific downstream behavior (auto-compact on idle) and explicitly notes that "Hooks don't have an idle or timer event to trigger on." That's the same gap from a different angle — they want one specific consumer of an idle event; this request asks for the underlying event primitive that #44789 (and others) could be implemented against.

Prior art elsewhere: VS Code's "Files: Auto Save: afterDelay" setting and most IDE auto-save patterns address the same problem on the editor side. Browser session-restore plus crash-recovery is the equivalent surface in web UIs. UserIdle would be the Claude Code analog.

Out of scope for this request:

  • The default threshold value (5m vs 10m vs configurable). User setting per hook group.
  • What a consuming hook DOES on fire (that's downstream of the event). Just give us the event.
  • Multi-fire / time-of-day predicates / activity-class differentiation. Single-fire-then-reset is the minimal useful shape.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions