{
  "markdown": "# Claude Code Idle Timing Plugin\n\nClaude Code plugin that injects hidden timing context alongside each user message.\n\n![Idle note on re-entry plus a live statusline timer tracking elapsed time since Claude's last reply](docs/screenshots/hero.png)\n\nThe plugin adds up to three fields inside a compact `[timing]` block:\n\n- `local_time` — local time with explicit UTC offset (only on the first prompt of a session)\n- `idle_for` — seconds idle since the assistant's last stop\n- `last_turn_dur` — seconds the previous assistant turn took to run\n\nEach prompt gets a hidden block Claude reads but you never see in your transcript:\n\n```\n[timing]\nlocal_time=2026-04-17T16:04:19+10:00\nidle_for=57.0s\nlast_turn_dur=88.2s\n[/timing]\n```\n\n## What It Does\n\nThe plugin uses official Claude Code hooks:\n\n- `UserPromptSubmit` injects hidden timing context on every prompt\n- `UserPromptSubmit` also shows a compact TUI note like `[after 5m 2s]` when the user replies after more than 10 seconds of idle time\n- `Stop` persists per-session timing state for the next turn\n- `PreCompact` resets the idle timer when context compaction runs, so the statusline counts from the compaction event rather than the last pre-compact reply\n\nOn a fresh session, unavailable prior-turn fields are omitted.\n\n## Install via Marketplace\n\n```text\n/plugin marketplace add clankercode/claude-inject-idle-time\n/plugin install idle-timing@idle-info\n```\n\n## Statusline integration (optional)\n\nThis plugin ships a composable fragment that prints the elapsed time since\nthe model's last reply. Two implementations are provided:\n\n- `scripts/statusline-fragment.sh` — POSIX-sh, no node cold-start. Recommended.\n  Reads a small per-session file (`.lastresponse`) that the hooks keep\n  updated, so it costs <10 ms per tick.\n- `scripts/statusline-fragment.js` — Node, with model-change tracking\n  (`---` when the current model differs from the one that produced the\n  last reply). Slower (~100 ms/tick) because of the node cold start and\n  because it still does read-modify-write on the session JSON. Kept for\n  reference and for users who want the `---` behavior.\n\nRun the slash command for a guided paste-ready snippet tailored to your current statusline:\n\n```text\n/idle-time-setup\n```\n\nAt a minimum you will need to:\n\n1. Enable periodic refresh in `~/.claude/settings.json`:\n\n    ```json\n    { \"statusLine\": { \"refreshInterval\": 1 } }\n    ```\n\n2. In your statusline script, after you read stdin into a variable (e.g. `input=$(cat)`), pipe the full stdin JSON to the fragment so it can see the current `session_id`:\n\n    ```bash\n    idle=$(echo \"$input\" | sh \"/path/to/idle-timing/scripts/statusline-fragment.sh\" 2>/dev/null || true)\n    [ -n \"$idle\" ] && parts+=(\"$idle\")\n    ```\n\nThe fragment prints just the elapsed time (e.g. `45s`, `3m 21s`, `17m`, `1h 23m`). Add any prefix or emoji in your own script.\n\nIf you want the model-change `---` behavior (and don't mind the per-tick node cold-start), swap the script path in the snippet above to `scripts/statusline-fragment.js` and run it via `node`.\n\nFlags (both fragments):\n\n- `.sh`: `--data-dir <path>`, `--drop-seconds-after <seconds>` (default 900, i.e. 15 minutes).\n- `.js`: `--session-id <id>`, `--model-id <id>`, `--drop-seconds-after <seconds>` (default 900).\n\n### Statusline state table\n\nThe fragment produces the following outputs:\n\n| State | Output | When |\n| --- | --- | --- |\n| No data dir / no session_id | (empty) | `CLAUDE_PLUGIN_DATA` unset or stdin has no `session_id` |\n| Fresh session, no prior turn | (empty) | `.lastresponse` file does not exist (first turn) |\n| Mid-turn, model unchanged | `<elapsed>` | Normal: counting up since the model's last reply |\n| Mid-turn, model changed | `---` | Current model differs from the one captured at the last stop (only with the `.js` fragment) |\n| After `/compact` | `<elapsed>` counting from compaction | `PreCompact` hook rewrites `.lastresponse` to the compaction timestamp |\n| Corrupt `.lastresponse` | (empty) | File exists but timestamp is unparseable; hook will rewrite it on next turn |\n\n## Observability\n\nThe plugin keeps all of its runtime state under the directory pointed to by the `CLAUDE_PLUGIN_DATA` environment variable (Claude Code sets this per session). Two subdirectories are created there:\n\n- `sessions/` — one `<sessionId>.json` file per session, holding the persisted timing state. The file format is a single-line JSON object with fields like `lastUserPromptAt`, `lastStopAt`, `lastAssistantMessageAt`, `lastTurnExecMs`, and `modelAtLastStop` / `modelAtLastStopAt`.\n- `logs/` — one `<sessionId>.log` file per session, holding NDJSON entries written by the plugin's error logger.\n\n### Error logging\n\nWhen a hook (UserPromptSubmit, Stop, or PreCompact) catches an unexpected error, the error is appended to `${CLAUDE_PLUGIN_DATA}/logs/<sessionId>.log` as a single NDJSON line. Each line has the shape:\n\n```json\n{\"ts\":\"2026-04-19T03:14:15.000Z\",\"hook\":\"UserPromptSubmit\",\"sessionId\":\"abc\",\"level\":\"error\",\"message\":\"...\",\"stack\":\"...\",\"context\":null}\n```\n\nThe original error stack is still written to stderr; Claude Code swallows that stream, so the log file is the user-visible diagnostic. The logger is best-effort and will not throw if the data dir or session id is missing.\n\n### Slash commands\n\n- `/idle-time-status` — runs a one-shot self-test. Reports the plugin version, the resolved data dir, the result of running each hook script, and the path to the per-session log file.\n- `/idle-time-reset` — clears the state and log files for the current session. With `--all --yes`, wipes every file in `${CLAUDE_PLUGIN_DATA}/sessions/` and `.../logs/`.\n\n### Inspecting state\n\n```bash\n# View the per-session state file\ncat \"${CLAUDE_PLUGIN_DATA}/sessions/${CLAUDE_SESSION_ID}.json\" | jq\n\n# Tail the most recent error log entries\ntail -n 20 \"${CLAUDE_PLUGIN_DATA}/logs/${CLAUDE_SESSION_ID}.log\"\n```\n\n## Local Usage\n\nRun Claude Code with the plugin from this repo root:\n\n```bash\nclaude --plugin-dir .\n```\n\nIf Claude Code is already running, reload plugins after changes:\n\n```text\n/reload-plugins\n```\n\n## Validation\n\nRun the automated test suite:\n\n```bash\nnpm test\n```\n\nValidate the plugin structure:\n\n```bash\nclaude plugin validate .\n```\n\nCount the tokens used by the timing block across representative payloads (uses `gpt-tokenizer` as a BPE proxy):\n\n```bash\nbun run tokens\n```\n\n## Configuration\n\nOptional settings live in `${CLAUDE_PLUGIN_DATA}/config.json` (the same directory the plugin already uses for per-session state). The file is read once per process; unknown keys are ignored with a warning, malformed JSON is treated as no overrides.\n\nKeys (with defaults):\n\n| Key | Default | Meaning |\n| --- | --- | --- |\n| `idleMessageThresholdSeconds` | `10` | Minimum idle gap (in seconds) before the visible `[after Xm Ys]` system message is shown. |\n| `idleMessageDropSecondsAfterSeconds` | `3600` | Once total idle seconds reaches this, the system message drops the trailing seconds — e.g. `[after 1h]` instead of `[after 1h 0m 0s]`. |\n| `dropSecondsAfterSeconds` | `900` | Default for the `statusline-fragment.js --drop-seconds-after` CLI flag (15 minutes). Another subagent wires this into the statusline fragment; for now the config key is exposed and read. |\n| `formatHoursAsDays` | `true` | When total idle seconds reaches a day, format the system message as `1d 4h` instead of `28h 0m`. |\n\nExample `config.json`:\n\n```json\n{\n  \"idleMessageThresholdSeconds\": 15,\n  \"idleMessageDropSecondsAfterSeconds\": 1800,\n  \"dropSecondsAfterSeconds\": 600,\n  \"formatHoursAsDays\": true\n}\n```\n\nNote: the statusline fragment's `drop-seconds-after` flag is a CLI override; the matching `dropSecondsAfterSeconds` config key here serves as its default once the statusline side starts reading `config.json`.\n\n## Notes\n\n- The timing block is added as hidden hook context, not visible prompt text.\n- The over-one-minute idle note is emitted as a hook `systemMessage` so it is visible to the user without being added to the plugin's `additionalContext`.\n- In v1, idle time is measured from the previous `Stop` hook timestamp.\n",
  "bytes": 8091,
  "sha": "a5991e8e990752b47529490245b9267950540c51130cc481fd51fe6ca4930249",
  "repo_slug": "clankercode/claude-inject-idle-time",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/plg_clankercode_claude_inject_idle_time_idle_68844a15/readme"
}