Skip to content

DoubleNode/claude-context-tick

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

claude-context-tick

CI License: MIT macOS Linux

A Claude Code UserPromptSubmit hook that injects wall-clock time into conversation context at meaningful boundaries.

asciicast: claude-context-tick gating logic in 4 scenes

~22 sec demo: first-run injection → silent rerun (state matches) → fast-forward state → quarter-hour-tick injection. Reproduce locally with bash scripts/demo.sh.

Why

Claude has no built-in concept of wall-clock time mid-conversation. The model's "today's date" comes from the system prompt at session start, but after that, subjective time inside the conversation drifts. Agents lose track of date rollovers, scheduled wake-ups, quarter-hourly pacing, and timezone shifts. This hook nudges Claude with a tiny ground-truth timestamp only when the answer to "what time is it actually" has materially changed — minimizing token cost while keeping the agent grounded.

What It Does

The hook injects a single-line context message <context-tick>YYYY-MM-DD · HH:MM TZ</context-tick> into the conversation only on state transitions that matter:

  • First run of a session — establishes baseline time
  • Date rollover — midnight boundary crossed
  • Quarter-hour tick — minute boundary hits 00, 15, 30, or 45
  • Timezone shift — DST transition or laptop traveling

State is tracked per-session in ~/.claude/state/time-inject/ with atomic writes (mktemp + mv) preventing partial state corruption on abnormal exits. The hook also performs lazy garbage collection of stale state files: files idle for ≥7 days are pruned on a 24-hour sweep cadence, and a complementary SessionEnd hook deletes a session's file immediately when the session ends cleanly.

Install

One-Line Install

curl -fsSL https://raw.githubusercontent.com/DoubleNode/claude-context-tick/main/scripts/install.sh | bash

Security note: Always review scripts before piping to bash. The script simply merges the hook entry into ~/.claude/settings.json.

Manual Install

git clone https://github.com/DoubleNode/claude-context-tick.git
cd claude-context-tick
bash scripts/install.sh

Hand-Merge settings.json

If you prefer to merge manually, copy the hooks entry from settings.example.json into your ~/.claude/settings.json:

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "${HOME}/.claude/hooks/inject-time-context.sh"
          }
        ]
      }
    ],
    "SessionEnd": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "${HOME}/.claude/hooks/session-end.sh"
          }
        ]
      }
    ]
  }
}

Ensure the hooks key exists; if not, create it at the top level.

Configuration

Environment variable to control behavior:

Variable Default Effect
CLAUDE_TIME_INJECT 1 Set to 0 to disable the hook entirely (all injections suppressed)

Example: CLAUDE_TIME_INJECT=0 claude launches a session with no time injections.

Note: Garbage collection behavior (7-day retention, 24-hour sweep cadence) is not currently configurable. Future versions may expose these via environment variables.

How It Decides Whether to Inject (State-Tracking Explainer)

On each prompt submission, the hook:

  1. Checks the kill-switch (CLAUDE_TIME_INJECT=0). If enabled, exits silently with no injection.
  2. Reads the current session_id from stdin (Claude Code UserPromptSubmit metadata). Sanitizes it via positive whitelist [A-Za-z0-9._-] so untrusted input cannot escape the state directory.
  3. Looks up the session's prior state in ~/.claude/state/time-inject/{SESSION_ID}.json. If the file doesn't exist, this is the first run.
  4. Evaluates four conditions in order:
    • First run? Inject full timestamp.
    • Timezone changed (DST or readlink /etc/localtime differs)? Inject.
    • Date changed (midnight crossed)? Inject.
    • Quarter-hour boundary (minute in [00, 15, 30, 45])? Inject.
  5. If any condition fires, write new state atomically (via mktemp, then mv) and emit the injection JSON to stdout per the UserPromptSubmit protocol.

The state file format is simple JSON:

{
  "date": "2026-05-06",
  "qh": "2026-05-06T14:30",
  "iana": "America/Los_Angeles",
  "tz": "PDT",
  "reason": "date-rollover"
}

The reason field documents why the injection fired (useful for debugging).

Garbage Collection

Without automated cleanup, every Claude Code session leaves behind a state file. Over weeks or months, a long-lived developer can accumulate hundreds of idle files, wasting disk space and cluttering the state directory.

The hook implements a two-pronged GC strategy:

SessionEnd hook — When a Claude Code session ends cleanly, the SessionEnd hook fires and deletes only that session's state file. This is precise, prompt, and race-free: no concurrent prompts, no sweep logic, just rm -f ~/.claude/state/time-inject/{SESSION_ID}.json.

Lazy sweep — Sessions that end abnormally (hard kill, OS crash, reboot before hook fires) leave orphan files. The inject-time-context.sh hook runs a backstop: once per 24 hours, it prunes any state file with mtime older than 7 days. This is rate-limited via a marker file (${STATE_DIR}/.gc-sweep) to avoid scanning the directory on every single prompt.

Liveness signal — Every prompt submission, even when no injection fires, touch-updates the state file's mtime. This ensures a long-running but quiet session (stuck on a single task for 8 hours within a quarter-hour window) is not mistakenly swept away.

Configuration — The retention threshold (7 days) and sweep interval (24 hours) are fixed at compile time. There are no environment variable overrides; this design choice prioritizes zero-config simplicity. If you need immediate cleanup, simply rm -rf ~/.claude/state/time-inject/ at any time. The hook will recreate the directory on its next run.

Uninstall

bash scripts/uninstall.sh

This removes both the UserPromptSubmit and SessionEnd hook entries from ~/.claude/settings.json, and deletes the installed hook scripts. The state directory ~/.claude/state/time-inject/ is left in place (can be manually removed if desired).

Troubleshooting

"I don't see any <context-tick> lines in my conversation"

The first prompt of any session should always fire an injection. Check ~/.claude/state/time-inject/ — you should see one .json file per session ID. If it's empty:

  • Verify ~/.claude/settings.json has an entry under hooks.UserPromptSubmit (PascalCase, an array).
  • Confirm the command field inside that entry's hooks array points to your inject-time-context.sh script (not a stale path).
  • Try the one-line install again to refresh the config.

"Hook fires but injection is missing from the conversation"

The hook ran but the state directory could not be written. This can happen if:

  • ~/.claude/state/ or ~/.claude/state/time-inject/ is read-only.
  • Disk is full.

The hook silently exits on write failures (by design — it is best-effort and should not spam the Claude Code UI with errors). Resolve the write issue, then start a new Claude Code session.

"Suspicious characters in my session_id warning"

Not a warning. D3 sanitization silently strips any character outside [A-Za-z0-9._-] from the session_id before using it as a filename. This is intentional and safe.

Security

The hook:

  • Reads only from stdin (session metadata from Claude Code).
  • Writes only to ~/.claude/state/time-inject/ (local state files, JSON format).
  • Never touches the network or makes external API calls.
  • Sanitizes session_id via positive whitelist before using it as a filename, preventing directory traversal attacks (e.g., ../etc/passwd injection).
  • Respects the kill-switch (CLAUDE_TIME_INJECT=0) before any I/O.

State files contain only timestamps and timezone metadata — no sensitive information.

Compatibility

Bash: 4.0+. macOS ships with bash 3.2 by default; this script uses [[ ... ]] and string comparisons compatible with 3.2. If you install bash 5+ via Homebrew, the shebang #!/usr/bin/env bash will pick it up automatically.

Python: 3.6+ required (already a Claude Code dependency). Used for robust JSON parsing and session_id sanitization.

Platforms: macOS (primary), Linux (Ubuntu, Debian tested; other distributions may vary). The hook tolerates non-symlink /etc/localtime on systems where readlink fails.

License

MIT. See LICENSE for details.

Contributing

Issues and pull requests welcome at https://github.com/DoubleNode/claude-context-tick/issues.

Tests live in tests/; contributions should include test cases. CI runs on macOS and Ubuntu via GitHub Actions.

About

Claude Code UserPromptSubmit hook: injects a tiny [context-tick] wall-clock line only when state changes (first-run, date-rollover, quarter-hour tick, timezone shift).

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors