idle-timing
- Injects timing block to prompts (not user-visible) covering local date-time, idle time (since agent turn ended), and prior turn execution
Open source Repository Open in the app JSON README (API)
About
- Injects timing block to prompts (not user-visible) covering local date-time, idle time (since agent turn ended), and prior turn execution time. - Shows `[after Xm Ys]` to user via post submit hook to indicate how long it was between that prompt and agent finishing prior turn. - Provides statusline integration for idle time.
Details
- Kind
- Plugins
- Topic
- No topic detected
- Publisher
- clankercode
- Origin
- marketplace
- Category
- ferramentas
- Stars
- 5
- Forks
- 1
- Last push
- 2026-06-11T15:23:16Z
- Repository state
- ativo
- Language
- JavaScript
- License
- NOASSERTION
- Added
- 2026-08-30 01:48:58
- Updated
- 2026-08-30 01:48:58
- Origin id
clankercode/claude-inject-idle-time/idle-timing
README
# Claude Code Idle Timing Plugin
Claude Code plugin that injects hidden timing context alongside each user message.

The plugin adds up to three fields inside a compact `[timing]` block:
- `local_time` — local time with explicit UTC offset (only on the first prompt of a session)
- `idle_for` — seconds idle since the assistant's last stop
- `last_turn_dur` — seconds the previous assistant turn took to run
Each prompt gets a hidden block Claude reads but you never see in your transcript:
```
[timing]
local_time=2026-04-17T16:04:19+10:00
idle_for=57.0s
last_turn_dur=88.2s
[/timing]
```
## What It Does
The plugin uses official Claude Code hooks:
- `UserPromptSubmit` injects hidden timing context on every prompt
- `UserPromptSubmit` also shows a compact TUI note like `[after 5m 2s]` when the user replies after more than 10 seconds of idle time
- `Stop` persists per-session timing state for the next turn
- `PreCompact` resets the idle timer when context compaction runs, so the statusline counts from the compaction event rather than the last pre-compact reply
On a fresh session, unavailable prior-turn fields are omitted.
## Install via Marketplace
```text
/plugin marketplace add clankercode/claude-inject-idle-time
/plugin install idle-timing@idle-info
```
## Statusline integration (optional)
This plugin ships a composable fragment that prints the elapsed time since
the model's last reply. Two implementations are provided:
- `scripts/statusline-fragment.sh` — POSIX-sh, no node cold-start. Recommended.
Reads a small per-session file (`.lastresponse`) that the hooks keep
updated, so it costs <10 ms per tick.
- `scripts/statusline-fragment.js` — Node, with model-change tracking
(`---` when the current model differs from the one that produced the
last reply). Slower (~100 ms/tick) because of the node cold start and
because it still does read-modify-write on the session JSON. Kept for
reference and for users who want the `---` behavior.
Run the slash command for a guided paste-ready snippet tailored to your current statusline:
```text
/idle-time-setup
```
At a minimum you will need to:
1. Enable periodic refresh in `~/.claude/settings.json`:
```json
{ "statusLine": { "refreshInterval": 1 } }
```
2. 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`:
```bash
idle=$(echo "$input" | sh "/path/to/idle-timing/scripts/statusline-fragment.sh" 2>/dev/null || true)
[ -n "$idle" ] && parts+=("$idle")
```
The fragment prints just the elapsed time (e.g. `45s`, `3m 21s`, `17m`, `1h 23m`). Add any prefix or emoji in your own script.
If 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`.
Flags (both fragments):
- `.sh`: `--data-dir <path>`, `--drop-seconds-after <seconds>` (default 900, i.e. 15 minutes).
- `.js`: `--session-id <id>`, `--model-id <id>`, `--drop-seconds-after <seconds>` (default 900).
### Statusline state table
The fragment produces the following outputs:
| State | Output | When |
| --- | --- | --- |
| No data dir / no session_id | (empty) | `CLAUDE_PLUGIN_DATA` unset or stdin has no `session_id` |
| Fresh session, no prior turn | (empty) | `.lastresponse` file does not exist (first turn) |
| Mid-turn, model unchanged | `<elapsed>` | Normal: counting up since the model's last reply |
| Mid-turn, model changed | `---` | Current model differs from the one captured at the last stop (only with the `.js` fragment) |
| After `/compact` | `<elapsed>` counting from compaction | `PreCompact` hook rewrites `.lastresponse` to the compaction timestamp |
| Corrupt `.lastresponse` | (empty) | File exists but timestamp is unparseable; hook will rewrite it on next turn |
## Observability
The 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:
- `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`.
- `logs/` — one `<sessionId>.log` file per session, holding NDJSON entries written by the plugin's error logger.
### Error logging
When 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:
```json
{"ts":"2026-04-19T03:14:15.000Z","hook":"UserPromptSubmit","sessionId":"abc","level":"error","message":"...","stack":"...","context":null}
```
The 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.
### Slash commands
- `/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.
- `/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/`.
### Inspecting state
```bash
# View the per-session state file
cat "${CLAUDE_PLUGIN_DATA}/sessions/${CLAUDE_SESSION_ID}.json" | jq
# Tail the most recent error log entries
tail -n 20 "${CLAUDE_PLUGIN_DATA}/logs/${CLAUDE_SESSION_ID}.log"
```
## Local Usage
Run Claude Code with the plugin from this repo root:
```bash
claude --plugin-dir .
```
If Claude Code is already running, reload plugins after changes:
```text
/reload-plugins
```
## Validation
Run the automated test suite:
```bash
npm test
```
Validate the plugin structure:
```bash
claude plugin validate .
```
Count the tokens used by the timing block across representative payloads (uses `gpt-tokenizer` as a BPE proxy):
```bash
bun run tokens
```
## Configuration
Optional 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.
Keys (with defaults):
| Key | Default | Meaning |
| --- | --- | --- |
| `idleMessageThresholdSeconds` | `10` | Minimum idle gap (in seconds) before the visible `[after Xm Ys]` system message is shown. |
| `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]`. |
| `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. |
| `formatHoursAsDays` | `true` | When total idle seconds reaches a day, format the system message as `1d 4h` instead of `28h 0m`. |
Example `config.json`:
```json
{
"idleMessageThresholdSeconds": 15,
"idleMessageDropSecondsAfterSeconds": 1800,
"dropSecondsAfterSeconds": 600,
"formatHoursAsDays": true
}
```
Note: 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`.
## Notes
- The timing block is added as hidden hook context, not visible prompt text.
- 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`.
- In v1, idle time is measured from the previous `Stop` hook timestamp.