
Chronos
- 7 installs
- 2 repo stars
- Updated August 3, 2026
- othmanadi/chronos
Helps with ai & agent building tasks during AI-assisted development.
About
chronos is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- chronos
- AI & Agent Building
- AI-coding skill
Chronos by the numbers
- 7 all-time installs (skills.sh)
- Ranked #12,520 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/othmanadi/chronos --skill chronosAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 7 |
|---|---|
| repo stars | ★ 2 |
| Last updated | August 3, 2026 |
| Repository | othmanadi/chronos ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Chronos — Time Awareness for Agents
You are not temporally blind. Stop guessing "when" and "how long ago". You have a ledger. Use it.
What chronos gives you
When the host platform supports hooks (Claude Code, Codex, OpenCode-plugin), chronos populates three sources of truth:
1. SessionStart baseline — injected into your context at session start:
now_utc— ISO-8601 UTCnow_local— ISO-8601 with offsettz— timezone name + offsetledger— absolute path to this session's JSONL tool-use ledgerstate— absolute path to this session's state filesession— session ID
2. Per-turn delta — injected on every user message:
now,turn,session_duration,since_last_user
3. Tool-use ledger — JSONL file, one entry per tool invocation:
{tool_use_id, tool, args_hash, started_at, finished_at, duration_ms, success}
If the host platform does not support hooks (Cursor, Hermes, ADAL, plain skill-only mode), chronos degrades: run date -u or date -Iseconds at decision points and state explicitly "derived from shell, not ledger".
When to consult time — 7 triggers
1. Before retrying a failed command
Read the ledger for the same tool + args_hash.
- If the last failure was
< 60sago with the same args, change strategy — don't re-run identically. - If
60s – 10min, consider one retry with a rationale. - If
> 10min, circumstances may have changed; re-run is reasonable.
Deploy cooldowns: the same rule applies to deployments and pushes. If the last deploy failed under 60s ago, check logs and fix config before re-deploying. Don't re-trigger a broken pipeline without a changed state.
2. Before trusting memory, a cached finding, or a prior read
Staleness thresholds by source:
| Source | Stale after | Action when stale |
|---|---|---|
git status, running processes, env vars | 5 min (same session) | Re-query |
| File contents previously Read | 1 hr OR any write to that path since Read | Re-Read |
| Persistent memory (MemPalace, KG facts) | 7 days since last write | Re-verify before use |
| External API responses (prices, PR status, issues) | 5 min | Re-fetch |
| Build/test results | After any source change since that run | Re-run |
| LLM's own prior claim ("earlier I saw...") | Always verify against ledger | Consult ledger |
3. Before reporting progress or writing a summary
Use session_duration to pick verbosity:
< 5 min— terse answer, no recap.5 – 30 min— one-paragraph summary of what changed.> 30 min— structured recap with timestamped milestones from the ledger.
4. Before running a long or destructive command
Scan the ledger for the same tool recently. If the last 3 duration_ms values for that tool are growing (e.g. each > 2× the previous median), something is degrading. Surface it, don't silently wait.
5. Idle-loop detection (autonomous mode)
Whenever since_last_user exceeds the idle threshold (default 900s / 15 min, configurable via CHRONOS_IDLE_THRESHOLD_SEC) and you are still in an autonomous loop: pause. Summarize progress, list pending decisions, ask for direction. This prevents silent runaway.
For explicit autonomous loops (/loop, /autonomous), the threshold may be higher — but always escalate when session_duration > 2h without user input.
6. Before any "wait" / "in a few minutes" / "just now" statement
Replace imprecise language with a concrete delta from the ledger:
- ❌ "Just tried that."
- ✅ "Last attempt was 47s ago (tool_use_id abc123, failed with EACCES)."
- ❌ "Let's wait a moment."
- ✅ "Retry at 2026-04-23T20:45:00Z (+3 min from now)."
7. Date or time questions
Never guess. In order of preference:
1. Read now_utc / now_local from the SessionStart context if session_duration < 10 min. 2. Read latest finished_at in the ledger if < 10 min ago. 3. Run date -u +"%Y-%m-%dT%H:%M:%SZ" fresh.
How to read the ledger
Ledger path is injected as ledger: <path> in SessionStart context.
One line = one event (JSONL, append-only). Latest line for a given tool_use_id wins.
# Last 10 events
tail -n 10 "$LEDGER_PATH"
# Events in last 10 minutes (helper script)
./scripts/ledger_read.sh --since 10m
# Events for a specific tool in last hour
./scripts/ledger_read.sh --tool Bash --since 1h
# Last duration for a specific args_hash
./scripts/ledger_read.sh --tool Bash --args-hash abc123 --lastEntry shape:
{"tool_use_id":"abc","tool":"Bash","args_hash":"f0e1","started_at":"2026-04-23T20:00:00Z"}
{"tool_use_id":"abc","tool":"Bash","args_hash":"f0e1","finished_at":"2026-04-23T20:00:12Z","duration_ms":12034,"success":true}Two lines per tool call: one on PreToolUse (started_at), one on PostToolUse (finished_at + duration). This is deliberate — async writes avoid adding latency.
args_hash is written by the PreToolUse hook: SHA-256 of the serialized tool arguments, first 12 hex characters. Filter the ledger by it to identify identical repeated calls. Do not compute it yourself — read it from the ledger only.
Failure modes to avoid
- Do not re-derive elapsed time by counting messages or turns. Use wall clock from ledger.
- Do not trust your own prior claim of "just now" from earlier in context. Ledger is the source of truth.
- Do not issue "try again in a few minutes" without a concrete ISO timestamp or delta.
- Do not silently hang in autonomous mode. Trigger #5 always applies.
- Do not assume the ledger exists. Check for
ledger:in your context first; degrade todate -uif absent.
Degraded mode (no hooks platform)
If you don't see chronos baseline in your context, the host platform doesn't support hooks. You still follow the decision rules — just swap ledger reads for:
- Current time:
date -u +"%Y-%m-%dT%H:%M:%SZ"(bash) orGet-Date -AsUTC -Format o(pwsh) - File mtime:
stat -c %Y <path>(bash) or(Get-Item <path>).LastWriteTimeUtc(pwsh) - Git age:
git log -1 --format=%cI <path>for last-commit ISO timestamp
State this explicitly: "No ledger on this platform; I'm using date -u as fallback."
Install / wiring
See installers/<platform>/ for platform-specific setup:
claude-code/— full hook stack (5 events)codex/— full hook stack (PreToolUse matches Bash only)opencode/— TS plugin writes ledger; read vialedger_read.shcursor/— rules-only degraded modehermes/,adal/,pi/,openclaw/— SKILL-only ports
Why this matters
From arxiv 2510.23853 "Your LLM Agents are Temporally Blind" (Oct 2025): even when timestamps are present in context, the best frontier models achieve only 65% alignment with human temporal judgment. Timestamps appear in fewer than 4% of reasoning traces. The fix is not more plumbing — it's explicit decision rules that make the agent look at the clock at the right moments.
This SKILL.md is those rules.
---
description: Time-awareness for coding agents. Consult shell clock + file mtimes before reasoning about "when" or "how long ago". No hooks on Cursor — degraded mode only.
alwaysApply: true
---
# chronos — time awareness (Cursor degraded mode)
Cursor has no hook API. You have no ledger. But the decision rules still apply.
## Rules
1. **Before retrying a failed command** — ask the user "how long ago did we try this?" if it's not obvious from the chat transcript. Don't re-run identically within 60s of the same failure.
2. **Before trusting prior reads or memory** —
- `git status`, running processes: stale after ~5 min of chat idle. Re-query.
- File you Read earlier: if the user edited anything since, re-Read.
- External API responses: stale after 5 min.
3. **Before reporting progress** — if the session feels long, write a structured recap with the concrete steps taken, not a vague summary.
4. **Idle-loop detection** — if you're in an agent loop and uncertain about user direction, pause and ask.
5. **Before saying "just now" / "recently" / "in a few minutes"** — get a concrete timestamp. Run a `date -u` shell command.
6. **Date/time questions** — never guess. Run `date -u +"%Y-%m-%dT%H:%M:%SZ"` or read a file's mtime with `stat -c %Y <path>`.
## How to get time on Cursor
Use the terminal tool. Bash:
```bash
date -u +"%Y-%m-%dT%H:%M:%SZ" # current UTC
stat -c %Y <path> # file mtime epoch
git log -1 --format=%cI <path> # last-commit ISO
```
PowerShell:
```powershell
Get-Date -AsUTC -Format o
(Get-Item <path>).LastWriteTimeUtc
```
## What you're missing without hooks
- No per-turn elapsed counter
- No session duration
- No tool-use ledger
- No idle detection
For full chronos, use Claude Code or Codex.
findings.md
*.chronos-backup-*
.DS_Store
Thumbs.db
chronos — Time Awareness (AGENTS.md mirror)
This file mirrors SKILL.md for agents that load AGENTS.md instead of or in addition to skills (Codex, ADAL, OpenClaw, Hermes, PI-mono).
See SKILL.md for the full spec. Quick reference below.
You have a ledger (or you don't)
If hooks are installed on your platform (Claude Code, Codex, OpenCode-plugin), you'll see chronos baseline in your context at session start with:
now_utc,now_local,tz,ledgerpath,statepath,sessionid
On every user turn:
now_utc,turn,session_duration,since_last_user
Tool-use ledger at ledger path is JSONL — one line per event:
- PreToolUse:
{tool_use_id, tool, args_hash, started_at, started_epoch} - PostToolUse:
{tool_use_id, tool, finished_at, finished_epoch, duration_ms, success}
If not installed: use date -u + stat -c %Y as fallback. State it explicitly.
7 triggers — when to consult time
1. Retry check. Same tool + args_hash failed < 60s ago → change strategy. 10min+ → re-run ok. 2. Staleness check. git/processes > 5min, files > 1h or modified-since, memory > 7 days, API > 5min. 3. Progress verbosity. < 5min terse, 5–30min paragraph, > 30min structured recap. 4. Long-command degradation. Last 3 durations growing? Surface, don't wait silently. 5. Idle-loop. since_last_user > 15min in autonomous mode → pause, summarize, ask. 6. No vague time. Replace "just now" / "a moment" with concrete delta from ledger. 7. Date questions. Read SessionStart baseline if fresh, else date -u.
Read the ledger
# last 10 min of events
scripts/ledger_read.sh --since 10m
# last 3 Bash runs
scripts/ledger_read.sh --tool Bash | tail -n 6
# specific args_hash
scripts/ledger_read.sh --tool Bash --args-hash abc123 --lastFailure modes
- Don't count turns for elapsed time — use wall clock.
- Don't trust your own "just now" from earlier in context.
- Don't hang silently in autonomous mode.
- If no ledger: say so, use shell fallback.
See SKILL.md for full detail.
Changelog
v1.0.0 (2026-04-24)
Initial release.
Core skill:
- SKILL.md with 7 temporal decision triggers for AI coding agents
- Staleness thresholds table by source type (git status, file contents, persistent memory, external APIs, build results)
- Degraded mode instructions for platforms without hook support
args_hashdocumentation for ledger-based retry detection
Hook stack:
- SessionStart: injects UTC baseline, timezone, ledger path, session ID
- UserPromptSubmit: per-turn elapsed counters (turn, session_duration, since_last_user)
- PreToolUse: records tool call start to JSONL ledger
- PostToolUse: records tool call finish with duration_ms and success flag
- Stop: idle detection for autonomous agents, configurable threshold
Platform support:
- Full hook stack: Claude Code, Codex CLI
- Partial (TS plugin or extension): OpenCode, PI, OpenClaw
- Skill-only degraded mode: Cursor, Hermes, ADAL
Scripts:
- bash and PowerShell parity for all 5 hooks plus ledger_read utility
- jq-optional on Windows via PowerShell ConvertFrom-Json fallback
- Ledger self-cleanup: gzip after 1 day, delete after 30 days
Installer:
- Non-destructive array concat merge into settings.json (no existing hook clobber)
- Backup before every write
--uninstallflag: removes chronos hooks and skill directory cleanly--dry-runflag: shows diff without writing- Project scope (
--project) and user scope support
Contributing
Adding a platform port
To add chronos support for a new agent platform:
1. Create installers/<platform>/README.md describing the platform's hook or plugin API 2. Write the context injection: SessionStart must emit now_utc, now_local, tz, ledger, state, session 3. Write per-turn injection: UserPromptSubmit must emit turn, session_duration, since_last_user 4. Write the ledger writer: PreToolUse appends started_at, PostToolUse appends finished_at + duration_ms + success 5. If the platform has no hook API, add a SKILL-only entry to the platform matrix in README.md with degraded mode instructions 6. Test with a pre-existing hook in the config (verify the installer does not clobber it)
Reporting a bug
Open an issue with:
- Platform and version (Claude Code 1.x, Codex CLI x.y, etc.)
- Shell (bash, zsh, PowerShell version)
- What you ran and what you expected
- The relevant ledger output if available (
tail ~/.chronos/ledger-*.jsonl)
Pull request rules
- No new runtime dependencies. bash, PowerShell, and optionally jq are the only allowed tools.
- All script changes must work on both bash and PowerShell (add
.ps1counterpart for any new.shscript) - Run the installer with a pre-existing hook in
~/.claude/settings.jsonand confirm it is preserved - Keep SKILL.md under 500 lines
- No Co-Authored-By in commits
chronos — ADAL (SylphAI adal-cli)
ADAL supports Agent Skills. Drop SKILL.md into the ADAL skills location (docs at https://docs.sylph.ai/).
Install
# path may vary by ADAL version — check docs
mkdir -p ~/.adal/skills/chronos
cp SKILL.md ~/.adal/skills/chronos/SKILL.md
cp AGENTS.md ~/.adal/skills/chronos/AGENTS.mdDegraded mode only
ADAL hook API is not publicly documented. Chronos runs in SKILL-only mode: decision rules + shell fallback (date -u).
#!/usr/bin/env bash
# chronos — Claude Code installer
# Concatenates chronos hooks into ~/.claude/settings.json (or .claude/settings.json with --project).
# Uses array concat per hook event so existing hooks (mempalace, planning-with-files, etc.) are preserved.
set -euo pipefail
CHRONOS_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
SCOPE=user
TARGET=""
UNINSTALL=false
DRY_RUN=false
while [ $# -gt 0 ]; do
case "$1" in
--project) SCOPE=project; shift ;;
--path) TARGET="$2"; shift 2 ;;
--uninstall) UNINSTALL=true; shift ;;
--dry-run) DRY_RUN=true; shift ;;
-h|--help)
cat <<'EOF'
Usage: install.sh [OPTIONS]
Options:
--project Install to .claude/settings.json (project scope) instead of user scope
--path PATH Install to a specific settings.json path
--uninstall Remove chronos hooks from settings.json and delete the skill directory
--dry-run Show what would change without writing anything
-h, --help Show this help
EOF
exit 0 ;;
*) echo "unknown option: $1" >&2; exit 2 ;;
esac
done
if [ -z "$TARGET" ]; then
if [ "$SCOPE" = project ]; then
mkdir -p .claude; TARGET=.claude/settings.json
else
mkdir -p "$HOME/.claude"; TARGET="$HOME/.claude/settings.json"
fi
fi
[ -f "$TARGET" ] || echo '{}' > "$TARGET"
if ! command -v jq >/dev/null 2>&1; then
echo "jq required. Install: https://jqlang.org" >&2; exit 2
fi
SKILL_DIR="$HOME/.claude/skills/chronos"
if [ "$SCOPE" = project ]; then SKILL_DIR=".claude/skills/chronos"; fi
# ── Uninstall ──────────────────────────────────────────────────────────────
if [ "$UNINSTALL" = true ]; then
BACKUP="$TARGET.chronos-backup-$(date +%s)"
cp "$TARGET" "$BACKUP"
TMP=$(mktemp)
jq '
if .hooks then
.hooks |= with_entries(
.value |= map(
. as $entry
| if (.command // "" | contains("chronos/scripts")) then empty
elif (.hooks // [] | map(.command // "") | map(contains("chronos/scripts")) | any) then empty
else . end
)
)
| .hooks |= with_entries(select(.value | length > 0))
else . end
' "$TARGET" > "$TMP"
if [ "$DRY_RUN" = true ]; then
echo "=== dry-run: hooks that would be removed ==="
diff <(jq '.hooks // {}' "$TARGET") <(jq '.hooks // {}' "$TMP") || true
rm -f "$TMP"
exit 0
fi
mv "$TMP" "$TARGET"
echo "chronos: removed hooks from $TARGET"
echo " backup: $BACKUP"
if [ -d "$SKILL_DIR" ]; then
rm -rf "$SKILL_DIR"
echo "chronos: removed skill directory $SKILL_DIR"
fi
exit 0
fi
# ── Install ────────────────────────────────────────────────────────────────
FRAGMENT="$CHRONOS_ROOT/installers/claude-code/settings.json"
BACKUP="$TARGET.chronos-backup-$(date +%s)"
cp "$TARGET" "$BACKUP"
# Strategy: per-event array concat. If chronos hook already present (by command substring), skip.
# Never replace existing user hooks.
TMP=$(mktemp)
jq -s --arg root "$CHRONOS_ROOT" '
(.[1] | walk(if type == "string" then gsub("\\$\\{CHRONOS_ROOT\\}"; $root) else . end) | del(._comment)) as $frag
| .[0] as $target
| $target
| .hooks = (.hooks // {})
| reduce ($frag.hooks | keys[]) as $event (.;
.hooks[$event] = (
(.hooks[$event] // [])
+ ($frag.hooks[$event] | map(
. as $new
| select(
([($target.hooks[$event] // [])[]
| (.hooks // [])[]
| .command // ""
] | map(contains("chronos/scripts")) | any) | not
)
))
)
)
' "$TARGET" "$FRAGMENT" > "$TMP"
if [ "$DRY_RUN" = true ]; then
echo "=== dry-run: hooks that would be added ==="
diff <(jq '.hooks // {}' "$TARGET") <(jq '.hooks // {}' "$TMP") || true
rm -f "$TMP"
echo " (no files written)"
exit 0
fi
mv "$TMP" "$TARGET"
echo "chronos: installed hooks (array concat, no clobbers) at $TARGET"
echo " backup: $BACKUP"
# Also install SKILL.md to ~/.claude/skills/chronos so it shows in the / menu.
mkdir -p "$SKILL_DIR"
cp "$CHRONOS_ROOT/SKILL.md" "$SKILL_DIR/SKILL.md"
if [ ! -e "$SKILL_DIR/scripts" ]; then
cp -r "$CHRONOS_ROOT/scripts" "$SKILL_DIR/scripts"
fi
echo "chronos: installed SKILL.md at $SKILL_DIR"
echo "CHRONOS_ROOT=$CHRONOS_ROOT"
echo "Restart Claude Code for /chronos to appear in the slash menu."
{
"_comment": "chronos — drop these entries into ~/.claude/settings.json or .claude/settings.json. Replace ${CHRONOS_ROOT} with the absolute path to your chronos checkout, e.g. /c/Users/you/Documents/here-now-projects/chronos. On Windows with Git Bash, .sh paths work. For pure PowerShell, use the .ps1 variants below and set shell to powershell.",
"hooks": {
"SessionStart": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "bash ${CHRONOS_ROOT}/scripts/session_start.sh" }
]
}
],
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "bash ${CHRONOS_ROOT}/scripts/prompt_submit.sh" }
]
}
],
"PreToolUse": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "bash ${CHRONOS_ROOT}/scripts/pre_tool.sh", "async": true }
]
}
],
"PostToolUse": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "bash ${CHRONOS_ROOT}/scripts/post_tool.sh", "async": true }
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "bash ${CHRONOS_ROOT}/scripts/stop.sh" }
]
}
]
}
}
{
"_comment": "chronos — Windows PowerShell variant. Replace ${CHRONOS_ROOT} with absolute Windows path (e.g. C:\\Users\\you\\Documents\\here-now-projects\\chronos).",
"hooks": {
"SessionStart": [
{ "matcher": "", "hooks": [ { "type": "command", "command": "powershell -ExecutionPolicy Bypass -File \"${CHRONOS_ROOT}\\scripts\\session_start.ps1\"" } ] }
],
"UserPromptSubmit": [
{ "matcher": "", "hooks": [ { "type": "command", "command": "powershell -ExecutionPolicy Bypass -File \"${CHRONOS_ROOT}\\scripts\\prompt_submit.ps1\"" } ] }
],
"PreToolUse": [
{ "matcher": "", "hooks": [ { "type": "command", "command": "powershell -ExecutionPolicy Bypass -File \"${CHRONOS_ROOT}\\scripts\\pre_tool.ps1\"", "async": true } ] }
],
"PostToolUse": [
{ "matcher": "", "hooks": [ { "type": "command", "command": "powershell -ExecutionPolicy Bypass -File \"${CHRONOS_ROOT}\\scripts\\post_tool.ps1\"", "async": true } ] }
],
"Stop": [
{ "matcher": "", "hooks": [ { "type": "command", "command": "powershell -ExecutionPolicy Bypass -File \"${CHRONOS_ROOT}\\scripts\\stop.ps1\"" } ] }
]
}
}
# chronos — Codex feature flag fragment.
# Append to ~/.codex/config.toml:
[features]
codex_hooks = true
{
"_comment": "chronos — Codex CLI hooks. Drop into ~/.codex/hooks.json (user) or <repo>/.codex/hooks.json. Also add [features] codex_hooks = true to ~/.codex/config.toml. Replace ${CHRONOS_ROOT}. Note: Codex PreToolUse matches Bash only; Read/Write/Edit tool events may be unavailable until coverage extends.",
"hooks": {
"SessionStart": [
{ "command": "bash ${CHRONOS_ROOT}/scripts/session_start.sh" }
],
"UserPromptSubmit": [
{ "command": "bash ${CHRONOS_ROOT}/scripts/prompt_submit.sh" }
],
"PreToolUse": [
{ "command": "bash ${CHRONOS_ROOT}/scripts/pre_tool.sh", "async": true }
],
"PostToolUse": [
{ "command": "bash ${CHRONOS_ROOT}/scripts/post_tool.sh", "async": true }
],
"Stop": [
{ "command": "bash ${CHRONOS_ROOT}/scripts/stop.sh" }
]
}
}
#!/usr/bin/env bash
# chronos — Codex CLI installer. Array-concat merge.
set -euo pipefail
CHRONOS_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
mkdir -p "$HOME/.codex"
TARGET="$HOME/.codex/hooks.json"
FRAGMENT="$CHRONOS_ROOT/installers/codex/hooks.json"
[ -f "$TARGET" ] || echo '{}' > "$TARGET"
if ! command -v jq >/dev/null 2>&1; then echo "jq required" >&2; exit 2; fi
BACKUP="$TARGET.chronos-backup-$(date +%s)"
cp "$TARGET" "$BACKUP"
TMP=$(mktemp)
jq -s --arg root "$CHRONOS_ROOT" '
(.[1] | walk(if type == "string" then gsub("\\$\\{CHRONOS_ROOT\\}"; $root) else . end) | del(._comment)) as $frag
| .[0] as $target
| $target
| .hooks = (.hooks // {})
| reduce ($frag.hooks | keys[]) as $event (.;
.hooks[$event] = (
(.hooks[$event] // [])
+ ($frag.hooks[$event] | map(
. as $new
| select(
([($target.hooks[$event] // [])[] | .command // ""]
| map(contains("chronos/scripts")) | any) | not
)
))
)
)
' "$TARGET" "$FRAGMENT" > "$TMP"
mv "$TMP" "$TARGET"
# Enable feature flag
CONF="$HOME/.codex/config.toml"
[ -f "$CONF" ] || touch "$CONF"
if ! grep -q '^\s*codex_hooks\s*=\s*true' "$CONF" 2>/dev/null; then
if ! grep -q '^\[features\]' "$CONF" 2>/dev/null; then
printf '\n[features]\ncodex_hooks = true\n' >> "$CONF"
else
awk '/^\[features\]/ {print; print "codex_hooks = true"; next} 1' "$CONF" > "$CONF.tmp" && mv "$CONF.tmp" "$CONF"
fi
fi
echo "chronos: Codex hooks installed (array concat) at $TARGET"
echo " backup: $BACKUP"
echo " enabled [features] codex_hooks=true"
echo "CHRONOS_ROOT=$CHRONOS_ROOT"
chronos — Cursor
Cursor has no hooks. SKILL-only degraded mode via .cursor/rules/chronos.mdc.
Install (project)
mkdir -p .cursor/rules
cp .cursor/rules/chronos.mdc .cursor/rules/chronos.mdcInstall (user global)
mkdir -p ~/.cursor/rules
cp .cursor/rules/chronos.mdc ~/.cursor/rules/chronos.mdcRule has alwaysApply: true — agent sees it every chat.
What you get
- Decision rules always in context
- Instructions to run
date -u+statfor time queries
What you don't get
No ledger, no per-turn delta, no idle detection.
#!/usr/bin/env bash
# chronos — shell detector. Picks bash or powershell variant based on env.
# Usage: eval "$(./installers/detect-shell.sh)"
# Exports: CHRONOS_SHELL=bash|powershell, CHRONOS_SETTINGS_FRAGMENT=<path>
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
if command -v bash >/dev/null 2>&1 && [ "${OS:-}" != "Windows_NT" ] || [ -n "${WSL_DISTRO_NAME:-}" ]; then
echo "export CHRONOS_SHELL=bash"
echo "export CHRONOS_SETTINGS_FRAGMENT=$ROOT/installers/claude-code/settings.json"
elif command -v pwsh >/dev/null 2>&1 || command -v powershell >/dev/null 2>&1; then
echo "export CHRONOS_SHELL=powershell"
echo "export CHRONOS_SETTINGS_FRAGMENT=$ROOT/installers/claude-code/settings.windows-ps.json"
else
echo "export CHRONOS_SHELL=bash"
echo "export CHRONOS_SETTINGS_FRAGMENT=$ROOT/installers/claude-code/settings.json"
fi
chronos — Hermes (NousResearch)
Hermes injects skills from ~/.hermes/skills/ as user messages. No shell hooks — SKILL-only mode.
Install
mkdir -p ~/.hermes/skills/chronos
cp SKILL.md ~/.hermes/skills/chronos/SKILL.mdAgent will follow decision rules using shell fallback (date -u, stat).
What you get
- Decision rules (7 triggers)
- Shell-fallback time queries
What you don't get
- Automatic ledger (no hooks)
- Per-turn elapsed inject
- Idle warnings
chronos — OpenClaw
OpenClaw uses skills/<name>/SKILL.md (Agent Skills standard).
Install
mkdir -p skills/chronos
cp SKILL.md skills/chronos/SKILL.md
cp AGENTS.md skills/chronos/AGENTS.mdOptional: plugin SDK for ledger
OpenClaw has a plugin-sdk (openclaw/plugin-sdk). Time-awareness plugin stub:
// See openclaw/plugin-sdk docs for full wiring.
// Hook into session + tool events, append to ~/.chronos/ledger-*.jsonlDegraded mode
SKILL-only works fine. Agent follows decision rules with shell fallback.
// chronos — OpenCode plugin
// Writes ledger + state to ~/.chronos. SKILL.md instructs agent to Read the state file at decision points.
// Install: drop into ~/.config/opencode/plugins/chronos.ts or <repo>/.opencode/plugins/chronos.ts
import type { Plugin } from "@opencode/plugin-sdk";
import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import * as crypto from "node:crypto";
const CHRONOS_HOME = process.env.CHRONOS_HOME ?? path.join(os.homedir(), ".chronos");
const IDLE_THRESHOLD_SEC = Number(process.env.CHRONOS_IDLE_THRESHOLD_SEC ?? 900);
fs.mkdirSync(CHRONOS_HOME, { recursive: true });
const nowUtc = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
const nowEpoch = () => Math.floor(Date.now() / 1000);
const tzName = () => Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC";
const argsHash = (s: string) => crypto.createHash("sha256").update(s).digest("hex").slice(0, 12);
const ledgerPath = (sid: string) => path.join(CHRONOS_HOME, `ledger-${sid}.jsonl`);
const statePath = (sid: string) => path.join(CHRONOS_HOME, `session-${sid}.json`);
const start: Record<string, number> = {};
export default {
name: "chronos",
version: "1.0.0",
onEvent: {
"session.created": async (ctx: any) => {
const sid = ctx.session?.id ?? `anon-${nowEpoch()}`;
fs.writeFileSync(ledgerPath(sid), "");
fs.writeFileSync(
statePath(sid),
JSON.stringify({
session_id: sid,
started_at_utc: nowUtc(),
started_at_epoch: nowEpoch(),
tz: tzName(),
turn: 0,
last_user_at_utc: nowUtc(),
last_user_at_epoch: nowEpoch(),
})
);
fs.writeFileSync(path.join(CHRONOS_HOME, "current-session"), sid);
},
"tool.execute.before": async (ctx: any) => {
const sid = ctx.session?.id ?? fs.readFileSync(path.join(CHRONOS_HOME, "current-session"), "utf8");
const tuid = ctx.toolUseId ?? `na-${nowEpoch()}`;
const tool = ctx.tool?.name ?? "unknown";
const input = JSON.stringify(ctx.tool?.input ?? {});
const hash = argsHash(input);
start[tuid] = nowEpoch();
const entry = {
tool_use_id: tuid,
tool,
args_hash: hash,
started_at: nowUtc(),
started_epoch: nowEpoch(),
};
fs.appendFileSync(ledgerPath(sid), JSON.stringify(entry) + "\n");
},
"tool.execute.after": async (ctx: any) => {
const sid = ctx.session?.id ?? fs.readFileSync(path.join(CHRONOS_HOME, "current-session"), "utf8");
const tuid = ctx.toolUseId ?? `na-${nowEpoch()}`;
const tool = ctx.tool?.name ?? "unknown";
const startedEpoch = start[tuid] ?? 0;
const durationMs = startedEpoch > 0 ? (nowEpoch() - startedEpoch) * 1000 : 0;
const success = !(ctx.result?.error || ctx.result?.is_error);
const entry = {
tool_use_id: tuid,
tool,
finished_at: nowUtc(),
finished_epoch: nowEpoch(),
duration_ms: durationMs,
success,
};
fs.appendFileSync(ledgerPath(sid), JSON.stringify(entry) + "\n");
delete start[tuid];
},
"session.idle": async (ctx: any) => {
const sid = ctx.session?.id;
if (!sid) return;
const state = JSON.parse(fs.readFileSync(statePath(sid), "utf8"));
const idle = nowEpoch() - (state.last_user_at_epoch ?? nowEpoch());
if (idle > IDLE_THRESHOLD_SEC) {
console.error(`chronos: idle ${idle}s > threshold ${IDLE_THRESHOLD_SEC}s`);
}
},
},
} satisfies Plugin;
chronos — OpenCode install
OpenCode plugins can't push arbitrary context into the model. Chronos works in "ledger-only" mode: the plugin writes the ledger + state file; SKILL.md instructs the agent to Read those files at decision points.
Install
mkdir -p ~/.config/opencode/plugins
cp installers/opencode/plugin.ts ~/.config/opencode/plugins/chronos.tsAlso copy the portable SKILL.md to a skills location OpenCode loads:
mkdir -p ~/.config/opencode/skills/chronos
cp SKILL.md ~/.config/opencode/skills/chronos/SKILL.mdHow the agent uses it
SKILL.md tells the agent to: 1. Read ~/.chronos/session-<sid>.json for baseline + last_user timestamp 2. Read ~/.chronos/ledger-<sid>.jsonl for tool-use history
No additionalContext injection on this platform — honest degraded mode.
Env
CHRONOS_HOME— override ledger location (default:~/.chronos)CHRONOS_IDLE_THRESHOLD_SEC— idle warning threshold (default: 900)
chronos — PI (badlogic/pi-mono)
PI supports Agent Skills. Copy SKILL.md into skills directory.
Install
mkdir -p ~/.pi/skills/chronos
cp SKILL.md ~/.pi/skills/chronos/SKILL.mdOptional: ledger via PI extension
PI exposes pi.on("tool_call", ...) event API. To add a ledger:
// example extension stub — see pi-mono docs for full wiring
import { pi } from "@badlogic/pi";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
const LEDGER = path.join(os.homedir(), ".chronos", "ledger-pi.jsonl");
pi.on("tool_call", (ev) => {
fs.appendFileSync(LEDGER, JSON.stringify({
tool: ev.name,
started_at: new Date().toISOString(),
duration_ms: ev.duration_ms,
success: !ev.error,
}) + "\n");
});Degraded mode
Without extension: SKILL rules still apply, shell-fallback.
MIT License
Copyright (c) 2026 Ahmad Othman Ammar Adi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
chronos
!How AI reacts when you ask what time it is
  
Time awareness for AI coding agents. Stops your agent from guessing "when" and "how long ago".
npx skills add OthmanAdi/chronos --skill chronos -gWorks across Claude Code, Codex, OpenCode, Cursor, PI, OpenClaw, Hermes, ADAL.
---
The problem
LLMs do not know what time it is. They guess. They report "just now" when it was two hours ago. They retry failed commands without checking that the last attempt was 10 seconds ago. They run silently in autonomous loops with no idle detection.
Arxiv 2510.23853 measured this empirically: even frontier models with timestamps injected into context reach only 65% temporal reasoning alignment with human judgment. Timestamps appear in fewer than 4% of reasoning traces. The ceiling exists because having access to timestamps is not enough. Agents need explicit rules for when to look at the clock.
Chronos is those rules, backed by a queryable tool-use ledger.
---
What chronos gives you
1. Portable decision rules — SKILL.md
Seven triggers that govern when an agent must consult time:
1. Before retrying a failed command: check the ledger for the last attempt and elapsed time 2. Before trusting memory or a cached result: apply staleness thresholds by source type 3. Before reporting progress: use session_duration to set summary verbosity 4. Before running a long or destructive command: detect tool degradation from ledger timing 5. Idle-loop detection: pause and surface decisions when the agent runs without user input too long 6. Before any "just now" or "in a minute" statement: replace with a concrete ISO delta from the ledger 7. Date and time questions: consult the baseline first, never guess
2. Hook-backed automation (Claude Code, Codex, OpenCode)
Four hook events populate three data sources on every session:
sequenceDiagram
participant A as Agent
participant H as Hook Scripts
participant L as ~/.chronos/
Note over A,L: Session start
H->>L: Initialize ledger + state file
H->>A: Inject baseline (now_utc, tz, ledger path, session ID)
Note over A,L: Each user message
H->>L: Increment turn counter, record last_user_at
H->>A: Inject per-turn delta (turn, session_duration, since_last_user)
Note over A,L: Each tool call
H->>L: Append started_at + args_hash (PreToolUse)
Note over H,L: Tool executes
H->>L: Append finished_at + duration_ms + success (PostToolUse)
Note over A,L: Session end
H->>A: Check idle time, emit warning if autonomous threshold exceeded3. Honest degraded mode (Cursor, Hermes, ADAL)
Platforms without hook support still get full decision rules. SKILL.md instructs the agent to use date -u, stat, and git log as fallback sources and to state explicitly which source it used.
---
Platform matrix
| Platform | Support | What you get |
|---|---|---|
| Claude Code | Full | All 5 hooks plus SKILL.md |
| Codex CLI | Full | All 5 hooks (PreToolUse targets Bash only) plus AGENTS.md |
| OpenCode | Partial | TypeScript plugin writes ledger; SKILL reads it |
| PI (pi-mono) | Partial | SKILL plus optional extension for ledger |
| OpenClaw | Partial | SKILL plus optional plugin-sdk for ledger |
| Cursor | Skill-only | Always-on rule with shell-fallback instructions |
| Hermes | Skill-only | SKILL injects as user message |
| ADAL | Skill-only | SKILL.md only (hook API not public) |
---
vs. prior art
| chronos | hodgesmr/temporal-awareness | temporal-awareness-mcp | |
|---|---|---|---|
| Decision rules | 7 triggers | None | None |
| Tool-use ledger | JSONL, per-call timing | None | None |
| Session baseline | SessionStart hook | Single date injection | API call per request |
| Per-turn elapsed | UserPromptSubmit hook | None | None |
| Idle detection | Stop hook | None | None |
| Degraded mode | Graceful fallback with explicit disclosure | N/A | N/A |
| Platforms | 8 | Claude Code only | Any MCP client |
| Shell | bash plus PowerShell | bash | Node.js |
| Install | ./install.sh (non-destructive merge) | Manual symlink | MCP client config |
hodgesmr/temporal-awareness is the prior Claude Code skill in this space. It shells date once at session start. No ledger, no decision rules, no multi-platform.
The MCP servers (pmbstyle/temporal-awareness-mcp, @vreme/temporal-mcp) provide time data to MCP-compatible clients. They do not ship decision rules and require separate MCP infrastructure.
---
Install
Claude Code (full hook stack)
git clone https://github.com/OthmanAdi/chronos ~/chronos
cd ~/chronos
./installers/claude-code/install.sh # user scope
./installers/claude-code/install.sh --project # project scope onlyMerges hooks into ~/.claude/settings.json without clobbering existing hooks. Backs up the original.
Verify: start a Claude Code session. You should see chronos baseline in context. Run a few tools, then:
tail ~/.chronos/ledger-*.jsonlCodex
./installers/codex/install.shInstalls ~/.codex/hooks.json and enables [features] codex_hooks = true in ~/.codex/config.toml.
OpenCode
mkdir -p ~/.config/opencode/plugins
cp installers/opencode/plugin.ts ~/.config/opencode/plugins/chronos.ts
mkdir -p ~/.config/opencode/skills/chronos
cp SKILL.md ~/.config/opencode/skills/chronos/SKILL.mdCursor
mkdir -p ~/.cursor/rules
cp .cursor/rules/chronos.mdc ~/.cursor/rules/Other platforms
See installers/<platform>/README.md.
Uninstall
./installers/claude-code/install.sh --uninstallRemoves all chronos hooks from settings.json, backs up the original, deletes ~/.claude/skills/chronos/.
---
Configuration
Set these environment variables before running Claude Code (or add to shell profile):
| Variable | Default | Effect |
|---|---|---|
CHRONOS_HOME | ~/.chronos | Ledger and state directory |
CHRONOS_IDLE_THRESHOLD_SEC | 900 | Seconds before idle warning fires in autonomous mode |
CHRONOS_LEDGER_RETENTION_DAYS | 30 | Delete ledgers older than this many days |
CHRONOS_LEDGER_GZIP_AFTER_DAYS | 1 | Compress ledgers older than this many days |
---
Usage
Query the ledger directly
# Last 10 events
tail -n 10 ~/.chronos/ledger-*.jsonl
# Events in the last 10 minutes
./scripts/ledger_read.sh --since 10m
# All Bash calls in the last hour
./scripts/ledger_read.sh --tool Bash --since 1h
# Count by tool type
./scripts/ledger_read.sh --since 1h | jq -s 'group_by(.tool) | map({tool:.[0].tool, count:length})'Check session state
cat ~/.chronos/session-$(cat ~/.chronos/current-session).json
# { "started_at_utc": "...", "turn": 12, "session_duration_sec": 847, ... }---
Ledger schema
JSONL, append-only. Two events per tool call:
{"tool_use_id":"abc","tool":"Bash","args_hash":"f0e1d2c3b4a5","started_at":"2026-04-23T20:00:00Z","started_epoch":1777233600}
{"tool_use_id":"abc","tool":"Bash","args_hash":"f0e1d2c3b4a5","finished_at":"2026-04-23T20:00:12Z","finished_epoch":1777233612,"duration_ms":12000,"success":true}args_hash is SHA-256 of the serialized tool arguments, first 12 hex characters. Use it to correlate start and finish events, and to detect repeated identical calls.
---
Architecture
chronos/
SKILL.md portable decision rules (primary)
AGENTS.md mirror for Codex, ADAL, OpenClaw, Hermes, PI
.cursor/rules/chronos.mdc Cursor degraded-mode rule
scripts/
_lib.sh _lib.ps1 shared library (time functions, ledger helpers)
session_start.sh .ps1 SessionStart hook
prompt_submit.sh .ps1 UserPromptSubmit hook
pre_tool.sh .ps1 PreToolUse hook
post_tool.sh .ps1 PostToolUse hook
stop.sh .ps1 Stop hook
ledger_read.sh .ps1 ledger query utility
installers/
claude-code/ flagship installer
codex/
opencode/
cursor/
hermes/ pi/ openclaw/ adal/
detect-shell.shRuntime state lives at ~/.chronos/:
ledger-<session>.jsonl: append-only tool-use event logsession-<session>.json: baseline, turn counter, last_user_at timestampcurrent-session: session ID fallback for platforms without stdin session_id injection
---
Design principles
- Honest degradation. If a platform cannot do hooks, SKILL.md tells the agent explicitly what fallback to use and requires disclosure.
- Async where possible. PreToolUse and PostToolUse run with
async: true, adding zero latency to tool calls. - Portable core. SKILL.md works on every supported platform. Hook scripts are an optional enhancement layer.
- Cross-platform parity. Every bash script has a PowerShell counterpart. No
jqrequired on Windows. - Self-cleaning. Ledgers compress after 1 day and delete after 30. No manual maintenance.
---
Why this was built
No existing tool combined: hook-backed timing, per-turn elapsed counters, a queryable tool-use ledger, and decision rules that change agent behavior, deployed across multiple agent platforms in a single installable skill.
Academic context: Your LLM Agents are Temporally Blind (Oct 2025) evaluated LLM temporal reasoning on the TicToc dataset (1,800 multi-turn dialogues, 76 scenario types). The measured gap exists even when frontier models have timestamps available. The fix is not more timestamps. It is explicit rules for when to consult them.
---
Credits
Built by OthmanAdi. MIT licensed.
Prior art and references:
- hodgesmr/temporal-awareness: prior Claude Code skill, date-shelling only
- Claude Code hooks: hook event reference
- Codex CLI hooks: Codex hook configuration
- arxiv 2510.23853: temporal blindness empirical study
- GitHub issue #24182: open request for native per-turn timestamps in Claude Code
---
License
MIT
# chronos — shared library (PowerShell)
# Dot-sourced by all hook scripts on Windows.
$ErrorActionPreference = 'Stop'
$script:ChronosHome = if ($env:CHRONOS_HOME) { $env:CHRONOS_HOME } else { Join-Path $HOME '.chronos' }
$script:IdleThresholdSec = if ($env:CHRONOS_IDLE_THRESHOLD_SEC) { [int]$env:CHRONOS_IDLE_THRESHOLD_SEC } else { 900 }
$script:RetentionDays = if ($env:CHRONOS_LEDGER_RETENTION_DAYS) { [int]$env:CHRONOS_LEDGER_RETENTION_DAYS } else { 30 }
$script:GzipAfterDays = if ($env:CHRONOS_LEDGER_GZIP_AFTER_DAYS) { [int]$env:CHRONOS_LEDGER_GZIP_AFTER_DAYS } else { 1 }
if (-not (Test-Path $script:ChronosHome)) { New-Item -ItemType Directory -Path $script:ChronosHome -Force | Out-Null }
function Get-NowUtcIso { (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") }
function Get-NowLocalIso { (Get-Date).ToString("yyyy-MM-ddTHH:mm:sszzz") }
function Get-NowEpoch { [int][double]::Parse((Get-Date -UFormat %s)) }
function Get-TzName {
try { [System.TimeZoneInfo]::Local.Id } catch { 'UTC' }
}
function Get-UtcOffset {
$o = [System.TimeZoneInfo]::Local.GetUtcOffset((Get-Date))
('{0}{1:D2}{2:D2}' -f $(if ($o.Ticks -ge 0) {'+'} else {'-'}), [Math]::Abs($o.Hours), [Math]::Abs($o.Minutes))
}
function Read-JsonField {
param([string]$Field, [string]$Input)
try {
$obj = $Input | ConvertFrom-Json -ErrorAction Stop
return $obj.$Field
} catch { return '' }
}
function Write-AdditionalContext {
param([string]$Event, [string]$Context)
$obj = @{
hookSpecificOutput = @{
hookEventName = $Event
additionalContext = $Context
}
}
$obj | ConvertTo-Json -Compress -Depth 5
}
function Get-LedgerPath { param($Session) Join-Path $script:ChronosHome "ledger-$Session.jsonl" }
function Get-StatePath { param($Session) Join-Path $script:ChronosHome "session-$Session.json" }
function Get-ArgsHash {
param([string]$Input)
$sha = [System.Security.Cryptography.SHA256]::Create()
$bytes = [System.Text.Encoding]::UTF8.GetBytes($Input)
$hash = $sha.ComputeHash($bytes)
([BitConverter]::ToString($hash) -replace '-','').ToLower().Substring(0,12)
}
function ConvertTo-DurationSec {
param([string]$D)
if ($D -match '^(\d+)([smhd])$') {
$n = [int]$Matches[1]; $u = $Matches[2]
switch ($u) { 's' {return $n}; 'm' {return $n*60}; 'h' {return $n*3600}; 'd' {return $n*86400} }
}
return [int]$D
}
function Invoke-LedgerCleanup {
Get-ChildItem -Path $script:ChronosHome -Filter 'ledger-*.jsonl' -ErrorAction SilentlyContinue | ForEach-Object {
$ageDays = (New-TimeSpan -Start $_.LastWriteTime -End (Get-Date)).Days
if ($ageDays -gt $script:RetentionDays) {
Remove-Item $_.FullName -Force -ErrorAction SilentlyContinue
Remove-Item "$($_.FullName).gz" -Force -ErrorAction SilentlyContinue
} elseif ($ageDays -gt $script:GzipAfterDays -and -not (Test-Path "$($_.FullName).gz")) {
try {
$bytes = [System.IO.File]::ReadAllBytes($_.FullName)
$out = New-Object System.IO.FileStream("$($_.FullName).gz"), Create, Write
$gz = New-Object System.IO.Compression.GZipStream($out, [System.IO.Compression.CompressionMode]::Compress)
$gz.Write($bytes, 0, $bytes.Length); $gz.Close(); $out.Close()
Remove-Item $_.FullName -Force
} catch { }
}
}
}
function Resolve-SessionId {
param([string]$Input)
$sid = Read-JsonField -Field 'session_id' -Input $Input
if (-not $sid) {
$file = Join-Path $script:ChronosHome 'current-session'
if (Test-Path $file) {
$sid = Get-Content $file -Raw
} else {
$sid = "anon-$(Get-NowEpoch)"
Set-Content -Path $file -Value $sid -NoNewline
}
}
$sid.Trim()
}
#!/usr/bin/env bash
# chronos — shared library (bash)
# Sourced by all hook scripts. Provides: now_iso, ledger_path, state_path, json helpers.
set -euo pipefail
CHRONOS_HOME="${CHRONOS_HOME:-$HOME/.chronos}"
CHRONOS_IDLE_THRESHOLD_SEC="${CHRONOS_IDLE_THRESHOLD_SEC:-900}" # 15 min
CHRONOS_LEDGER_RETENTION_DAYS="${CHRONOS_LEDGER_RETENTION_DAYS:-30}"
CHRONOS_LEDGER_GZIP_AFTER_DAYS="${CHRONOS_LEDGER_GZIP_AFTER_DAYS:-1}"
mkdir -p "$CHRONOS_HOME"
now_utc_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
now_local_iso() { date +"%Y-%m-%dT%H:%M:%S%z"; }
now_epoch() { date +%s; }
tz_name() { date +%Z 2>/dev/null || echo "UTC"; }
utc_offset() { date +%z 2>/dev/null || echo "+0000"; }
# Read field from stdin JSON using jq if available, else python fallback.
read_json_field() {
local field="$1" input="$2"
if command -v jq >/dev/null 2>&1; then
printf '%s' "$input" | jq -r ".$field // empty"
elif command -v python3 >/dev/null 2>&1; then
printf '%s' "$input" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$field',''))"
elif command -v python >/dev/null 2>&1; then
printf '%s' "$input" | python -c "import sys,json; d=json.load(sys.stdin); print(d.get('$field',''))"
else
echo "chronos: need jq or python" >&2; return 1
fi
}
# Emit additionalContext JSON for Claude Code / Codex hooks.
emit_additional_context() {
local event="$1" ctx="$2"
if command -v jq >/dev/null 2>&1; then
jq -n --arg e "$event" --arg c "$ctx" \
'{hookSpecificOutput:{hookEventName:$e, additionalContext:$c}}'
else
# Minimal JSON escape for quotes + newlines; good enough for ASCII context.
local esc=${ctx//\\/\\\\}
esc=${esc//\"/\\\"}
esc=${esc//$'\n'/\\n}
printf '{"hookSpecificOutput":{"hookEventName":"%s","additionalContext":"%s"}}\n' "$event" "$esc"
fi
}
ledger_path() { local session="$1"; echo "$CHRONOS_HOME/ledger-$session.jsonl"; }
state_path() { local session="$1"; echo "$CHRONOS_HOME/session-$session.json"; }
# sha256 of stdin, short (12 hex).
args_hash() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum | awk '{print substr($1,1,12)}'
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 | awk '{print substr($1,1,12)}'
else
awk '{s+=length($0)} END {printf "%012x\n", s}' # crude fallback, length-based
fi
}
# Human-duration → seconds. 10m, 2h, 30s, 1d.
parse_duration_to_sec() {
local d="$1"
local n=${d%[smhd]}
local u=${d: -1}
case "$u" in
s) echo "$n" ;;
m) echo "$((n*60))" ;;
h) echo "$((n*3600))" ;;
d) echo "$((n*86400))" ;;
*) echo "$d" ;; # assume seconds
esac
}
# ISO → epoch (uses date -d if available; works on GNU + BSD via `date -j` fallback).
iso_to_epoch() {
local iso="$1"
if date -d "$iso" +%s >/dev/null 2>&1; then
date -d "$iso" +%s
elif date -j -f "%Y-%m-%dT%H:%M:%SZ" "$iso" +%s >/dev/null 2>&1; then
date -j -f "%Y-%m-%dT%H:%M:%SZ" "$iso" +%s
else
echo 0
fi
}
# Cleanup old ledgers. Gzip > N days, delete > M days.
cleanup_ledgers() {
local now today_epoch
today_epoch=$(date +%s)
find "$CHRONOS_HOME" -name 'ledger-*.jsonl' -type f 2>/dev/null | while read -r f; do
local mtime
mtime=$(stat -c %Y "$f" 2>/dev/null || stat -f %m "$f" 2>/dev/null || echo "$today_epoch")
local age_days=$(( (today_epoch - mtime) / 86400 ))
if [ "$age_days" -gt "$CHRONOS_LEDGER_RETENTION_DAYS" ]; then
rm -f "$f" "${f%.jsonl}.jsonl.gz" 2>/dev/null || true
elif [ "$age_days" -gt "$CHRONOS_LEDGER_GZIP_AFTER_DAYS" ] && [ ! -f "${f}.gz" ]; then
gzip -q "$f" 2>/dev/null || true
fi
done
}
# Get session ID from stdin JSON, or fall back to persistent file.
resolve_session_id() {
local input="$1"
local sid
sid=$(read_json_field session_id "$input" 2>/dev/null || true)
if [ -z "${sid:-}" ]; then
local file="$CHRONOS_HOME/current-session"
if [ -f "$file" ]; then
sid=$(cat "$file")
else
sid="anon-$(now_epoch)"
echo "$sid" > "$file"
fi
fi
echo "$sid"
}
# chronos — ledger reader (PowerShell)
#
# Usage:
# ledger_read.ps1 [-Session SID] [-Tool NAME] [-ArgsHash HASH] [-Since 10m|1h|2d] [-Last] [-Count] [-PathOnly]
[CmdletBinding()]
param(
[string]$Session,
[string]$Tool,
[string]$ArgsHash,
[string]$Since,
[switch]$Last,
[switch]$Count,
[switch]$PathOnly
)
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot '_lib.ps1')
if (-not $Session) {
$cur = Join-Path $script:ChronosHome 'current-session'
if (Test-Path $cur) { $Session = (Get-Content $cur -Raw).Trim() }
else { Write-Error 'no session — pass -Session SID'; exit 2 }
}
$Ledger = Get-LedgerPath -Session $Session
if ($PathOnly) { Write-Output $Ledger; exit 0 }
if (-not (Test-Path $Ledger)) { Write-Error "no ledger at $Ledger"; exit 0 }
$NowEpoch = Get-NowEpoch
$CutoffEpoch = 0
if ($Since) { $CutoffEpoch = $NowEpoch - (ConvertTo-DurationSec -D $Since) }
$Entries = Get-Content $Ledger | ForEach-Object {
try { $_ | ConvertFrom-Json -ErrorAction Stop } catch { $null }
} | Where-Object { $_ }
if ($Tool) { $Entries = $Entries | Where-Object { $_.tool -eq $Tool } }
if ($ArgsHash) { $Entries = $Entries | Where-Object { $_.args_hash -eq $ArgsHash } }
if ($CutoffEpoch -gt 0) {
$Entries = $Entries | Where-Object {
$ep = if ($_.started_epoch) { [int]$_.started_epoch } elseif ($_.finished_epoch) { [int]$_.finished_epoch } else { 0 }
$ep -ge $CutoffEpoch
}
}
if ($Last) { $Entries = $Entries | Select-Object -Last 1 }
if ($Count) { Write-Output ($Entries | Measure-Object).Count; exit 0 }
$Entries | ForEach-Object { $_ | ConvertTo-Json -Compress }
#!/usr/bin/env bash
# chronos — ledger reader. Query ledger by tool, age, args_hash.
#
# Usage:
# ledger_read.sh [--session SID] [--tool NAME] [--args-hash HASH] [--since DURATION] [--last] [--count] [--path-only]
#
# Examples:
# ledger_read.sh --since 10m
# ledger_read.sh --tool Bash --since 1h
# ledger_read.sh --tool Bash --args-hash abc123 --last
# ledger_read.sh --count
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$SCRIPT_DIR/_lib.sh"
SESSION=""
TOOL=""
ARGS_HASH=""
SINCE=""
LAST=0
COUNT=0
PATH_ONLY=0
while [ $# -gt 0 ]; do
case "$1" in
--session) SESSION="$2"; shift 2 ;;
--tool) TOOL="$2"; shift 2 ;;
--args-hash) ARGS_HASH="$2"; shift 2 ;;
--since) SINCE="$2"; shift 2 ;;
--last) LAST=1; shift ;;
--count) COUNT=1; shift ;;
--path-only) PATH_ONLY=1; shift ;;
-h|--help) sed -n '2,12p' "$0"; exit 0 ;;
*) echo "unknown: $1" >&2; exit 2 ;;
esac
done
if [ -z "$SESSION" ]; then
if [ -f "$CHRONOS_HOME/current-session" ]; then
SESSION=$(cat "$CHRONOS_HOME/current-session")
else
echo "no session — pass --session SID" >&2; exit 2
fi
fi
LEDGER=$(ledger_path "$SESSION")
if [ "$PATH_ONLY" = 1 ]; then echo "$LEDGER"; exit 0; fi
if [ ! -f "$LEDGER" ]; then echo "no ledger at $LEDGER" >&2; exit 0; fi
NOW_EPOCH=$(now_epoch)
CUTOFF_EPOCH=0
if [ -n "$SINCE" ]; then
SEC=$(parse_duration_to_sec "$SINCE")
CUTOFF_EPOCH=$(( NOW_EPOCH - SEC ))
fi
if ! command -v jq >/dev/null 2>&1; then
echo "jq required for ledger_read" >&2; exit 2
fi
FILTER='.'
[ -n "$TOOL" ] && FILTER="$FILTER | select(.tool == \"$TOOL\")"
[ -n "$ARGS_HASH" ] && FILTER="$FILTER | select(.args_hash == \"$ARGS_HASH\")"
if [ "$CUTOFF_EPOCH" -gt 0 ]; then
FILTER="$FILTER | select((.started_epoch // .finished_epoch // 0) >= $CUTOFF_EPOCH)"
fi
OUT=$(jq -c "$FILTER" "$LEDGER" 2>/dev/null)
if [ "$LAST" = 1 ]; then OUT=$(printf '%s\n' "$OUT" | tail -n 1); fi
if [ "$COUNT" = 1 ]; then printf '%s\n' "$OUT" | grep -c '^' || echo 0; exit 0; fi
printf '%s\n' "$OUT"
# chronos — PostToolUse hook (PowerShell)
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot '_lib.ps1')
$Input = [Console]::In.ReadToEnd()
$Session = Resolve-SessionId -Input $Input
$Ledger = Get-LedgerPath -Session $Session
$NowUtc = Get-NowUtcIso
$NowEpoch = Get-NowEpoch
$Obj = $Input | ConvertFrom-Json
$Tool = if ($Obj.tool_name) { $Obj.tool_name } else { 'unknown' }
$TuId = if ($Obj.tool_use_id) { $Obj.tool_use_id } else { "na-$NowEpoch" }
$StartedEpoch = 0
if (Test-Path $Ledger) {
$Lines = Get-Content $Ledger
foreach ($line in $Lines) {
try {
$e = $line | ConvertFrom-Json -ErrorAction Stop
if ($e.tool_use_id -eq $TuId -and $e.started_epoch) {
$StartedEpoch = [int]$e.started_epoch
}
} catch {}
}
}
$DurationMs = if ($StartedEpoch -gt 0) { ($NowEpoch - $StartedEpoch) * 1000 } else { 0 }
$Success = $true
if ($Obj.tool_response) {
if ($Obj.tool_response.is_error -or $Obj.tool_response.error) { $Success = $false }
}
$Entry = @{
tool_use_id = $TuId
tool = $Tool
finished_at = $NowUtc
finished_epoch = $NowEpoch
duration_ms = $DurationMs
success = $Success
} | ConvertTo-Json -Compress
Add-Content -Path $Ledger -Value $Entry
exit 0
#!/usr/bin/env bash
# chronos — PostToolUse hook. Runs async.
# Appends finished_at + duration_ms entry to ledger.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$SCRIPT_DIR/_lib.sh"
INPUT=$(cat)
SESSION=$(resolve_session_id "$INPUT")
LEDGER=$(ledger_path "$SESSION")
NOW_UTC=$(now_utc_iso)
NOW_EPOCH=$(now_epoch)
TOOL=$(read_json_field tool_name "$INPUT" 2>/dev/null || echo unknown)
TOOL_USE_ID=$(read_json_field tool_use_id "$INPUT" 2>/dev/null || echo "na-$NOW_EPOCH")
# Find matching started_epoch from ledger (last match wins)
STARTED_EPOCH=0
if [ -f "$LEDGER" ] && command -v jq >/dev/null 2>&1; then
STARTED_EPOCH=$(grep -F "\"tool_use_id\":\"$TOOL_USE_ID\"" "$LEDGER" 2>/dev/null | \
jq -s 'map(select(.started_epoch != null)) | last.started_epoch // 0' 2>/dev/null || echo 0)
fi
DURATION_MS=0
if [ "${STARTED_EPOCH:-0}" -gt 0 ]; then
DURATION_MS=$(( (NOW_EPOCH - STARTED_EPOCH) * 1000 ))
fi
# Detect success: Claude Code passes tool_response; if it has .error or is_error true, fail.
SUCCESS=true
if command -v jq >/dev/null 2>&1; then
IS_ERR=$(printf '%s' "$INPUT" | jq -r '.tool_response.is_error // .tool_response.error // empty' 2>/dev/null || true)
if [ -n "${IS_ERR:-}" ] && [ "$IS_ERR" != "null" ] && [ "$IS_ERR" != "false" ]; then
SUCCESS=false
fi
fi
if command -v jq >/dev/null 2>&1; then
jq -nc \
--arg id "$TOOL_USE_ID" --arg t "$TOOL" \
--arg fa "$NOW_UTC" --argjson fe "$NOW_EPOCH" \
--argjson dms "$DURATION_MS" --argjson ok "$SUCCESS" \
'{tool_use_id:$id, tool:$t, finished_at:$fa, finished_epoch:$fe, duration_ms:$dms, success:$ok}' \
>> "$LEDGER"
else
printf '{"tool_use_id":"%s","tool":"%s","finished_at":"%s","finished_epoch":%s,"duration_ms":%s,"success":%s}\n' \
"$TOOL_USE_ID" "$TOOL" "$NOW_UTC" "$NOW_EPOCH" "$DURATION_MS" "$SUCCESS" >> "$LEDGER"
fi
exit 0
# chronos — PreToolUse hook (PowerShell)
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot '_lib.ps1')
$Input = [Console]::In.ReadToEnd()
$Session = Resolve-SessionId -Input $Input
$Ledger = Get-LedgerPath -Session $Session
$NowUtc = Get-NowUtcIso
$NowEpoch = Get-NowEpoch
$Obj = $Input | ConvertFrom-Json
$Tool = if ($Obj.tool_name) { $Obj.tool_name } else { 'unknown' }
$TuId = if ($Obj.tool_use_id) { $Obj.tool_use_id } else { "na-$NowEpoch" }
$ToolInput = if ($Obj.tool_input) { $Obj.tool_input | ConvertTo-Json -Compress -Depth 10 } else { '{}' }
$Hash = Get-ArgsHash -Input $ToolInput
$Entry = @{
tool_use_id = $TuId
tool = $Tool
args_hash = $Hash
started_at = $NowUtc
started_epoch = $NowEpoch
} | ConvertTo-Json -Compress
Add-Content -Path $Ledger -Value $Entry
exit 0
#!/usr/bin/env bash
# chronos — PreToolUse hook. Runs async (settings.json must set async: true).
# Appends started_at entry to ledger. Never blocks the agent.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$SCRIPT_DIR/_lib.sh"
INPUT=$(cat)
SESSION=$(resolve_session_id "$INPUT")
LEDGER=$(ledger_path "$SESSION")
NOW_UTC=$(now_utc_iso)
NOW_EPOCH=$(now_epoch)
TOOL=$(read_json_field tool_name "$INPUT" 2>/dev/null || echo unknown)
TOOL_USE_ID=$(read_json_field tool_use_id "$INPUT" 2>/dev/null || echo "na-$NOW_EPOCH")
# args_hash over tool_input (compact)
TOOL_INPUT=""
if command -v jq >/dev/null 2>&1; then
TOOL_INPUT=$(printf '%s' "$INPUT" | jq -c '.tool_input // {}' 2>/dev/null || echo "{}")
fi
ARGS_HASH=$(printf '%s' "$TOOL_INPUT" | args_hash)
# Append JSONL entry
if command -v jq >/dev/null 2>&1; then
jq -nc \
--arg id "$TOOL_USE_ID" --arg t "$TOOL" --arg h "$ARGS_HASH" \
--arg sa "$NOW_UTC" --argjson se "$NOW_EPOCH" \
'{tool_use_id:$id, tool:$t, args_hash:$h, started_at:$sa, started_epoch:$se}' \
>> "$LEDGER"
else
printf '{"tool_use_id":"%s","tool":"%s","args_hash":"%s","started_at":"%s","started_epoch":%s}\n' \
"$TOOL_USE_ID" "$TOOL" "$ARGS_HASH" "$NOW_UTC" "$NOW_EPOCH" >> "$LEDGER"
fi
# Silent exit. No context inject — keeps PreToolUse fast and non-blocking.
exit 0
# chronos — UserPromptSubmit hook (PowerShell)
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot '_lib.ps1')
$RawInput = [Console]::In.ReadToEnd()
$Session = Resolve-SessionId -Input $RawInput
$State = Get-StatePath -Session $Session
$Ledger = Get-LedgerPath -Session $Session
# Bootstrap BEFORE capturing NOW (otherwise bootstrap writes a later started_at → negative deltas).
if (-not (Test-Path $State)) {
& (Join-Path $PSScriptRoot 'session_start.ps1') -JsonInput $RawInput | Out-Null
}
$NowUtc = Get-NowUtcIso
$NowEpoch = Get-NowEpoch
$PrevLastEpoch = 0; $PrevStartEpoch = $NowEpoch; $PrevTurn = 0
if (Test-Path $State) {
$S = Get-Content $State -Raw | ConvertFrom-Json
$PrevLastEpoch = [int]$S.last_user_at_epoch
$PrevStartEpoch = [int]$S.started_at_epoch
$PrevTurn = [int]$S.turn
}
$SinceLast = $NowEpoch - $PrevLastEpoch
$SessionDuration = $NowEpoch - $PrevStartEpoch
$NewTurn = $PrevTurn + 1
if ($SinceLast -lt 0) { $SinceLast = 0 }
if ($SessionDuration -lt 0) { $SessionDuration = 0 }
$S.last_user_at_utc = $NowUtc
$S.last_user_at_epoch = $NowEpoch
$S.turn = $NewTurn
$S | ConvertTo-Json -Compress | Set-Content -Path $State -NoNewline
function Format-Duration($s) {
if ($s -lt 60) { "${s}s" }
elseif ($s -lt 3600) { "$([int]($s/60))m $($s%60)s" }
elseif ($s -lt 86400) { "$([int]($s/3600))h $([int](($s%3600)/60))m" }
else { "$([int]($s/86400))d $([int](($s%86400)/3600))h" }
}
$SinceH = Format-Duration $SinceLast
$SessionH = Format-Duration $SessionDuration
$IdleWarn = ''
if ($SinceLast -gt $script:IdleThresholdSec -and $NewTurn -gt 1) {
$IdleWarn = "`nidle_warning: user was away for $SinceH — if running autonomously, re-confirm direction."
}
$Ctx = @"
chronos turn
now_utc: $NowUtc
turn: $NewTurn
session_duration: $SessionH ($SessionDuration s)
since_last_user: $SinceH ($SinceLast s)
ledger: $Ledger$IdleWarn
"@
Write-AdditionalContext -Event 'UserPromptSubmit' -Context $Ctx
exit 0
#!/usr/bin/env bash
# chronos — UserPromptSubmit hook
# Injects per-turn elapsed delta into agent context.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$SCRIPT_DIR/_lib.sh"
INPUT=$(cat)
SESSION=$(resolve_session_id "$INPUT")
STATE=$(state_path "$SESSION")
LEDGER=$(ledger_path "$SESSION")
# If state missing, bootstrap (hook ordering edge case) BEFORE capturing NOW.
# If we captured NOW first, bootstrap would write a later started_at_epoch → negative deltas.
if [ ! -f "$STATE" ]; then
"$SCRIPT_DIR/session_start.sh" <<< "$INPUT" >/dev/null 2>&1 || true
fi
NOW_UTC=$(now_utc_iso)
NOW_EPOCH=$(now_epoch)
# Clamp: if state's started_at is in the future (bootstrap race or clock adjust),
# treat this as turn 0 with zero deltas rather than negative.
# Read previous state
PREV_LAST_USER_EPOCH=0
PREV_START_EPOCH="$NOW_EPOCH"
PREV_TURN=0
if [ -f "$STATE" ] && command -v jq >/dev/null 2>&1; then
PREV_LAST_USER_EPOCH=$(jq -r '.last_user_at_epoch // 0' "$STATE")
PREV_START_EPOCH=$(jq -r '.started_at_epoch // 0' "$STATE")
PREV_TURN=$(jq -r '.turn // 0' "$STATE")
fi
SINCE_LAST=$(( NOW_EPOCH - PREV_LAST_USER_EPOCH ))
SESSION_DURATION=$(( NOW_EPOCH - PREV_START_EPOCH ))
NEW_TURN=$(( PREV_TURN + 1 ))
# Clamp negatives to 0 (bootstrap race / clock skew).
[ "$SINCE_LAST" -lt 0 ] && SINCE_LAST=0
[ "$SESSION_DURATION" -lt 0 ] && SESSION_DURATION=0
# Write back
if command -v jq >/dev/null 2>&1; then
TMP=$(mktemp)
jq --arg u "$NOW_UTC" --argjson e "$NOW_EPOCH" --argjson t "$NEW_TURN" \
'.last_user_at_utc = $u | .last_user_at_epoch = $e | .turn = $t' \
"$STATE" > "$TMP" && mv "$TMP" "$STATE"
fi
# Humanize deltas
humanize() {
local s=$1
if [ "$s" -lt 60 ]; then echo "${s}s"
elif [ "$s" -lt 3600 ]; then echo "$((s/60))m $((s%60))s"
elif [ "$s" -lt 86400 ]; then echo "$((s/3600))h $(((s%3600)/60))m"
else echo "$((s/86400))d $(((s%86400)/3600))h"
fi
}
SINCE_LAST_H=$(humanize "$SINCE_LAST")
SESSION_H=$(humanize "$SESSION_DURATION")
IDLE_WARN=""
if [ "$SINCE_LAST" -gt "$CHRONOS_IDLE_THRESHOLD_SEC" ] && [ "$NEW_TURN" -gt 1 ]; then
IDLE_WARN="
idle_warning: user was away for $SINCE_LAST_H — if running autonomously, re-confirm direction."
fi
CTX="chronos turn
now_utc: $NOW_UTC
turn: $NEW_TURN
session_duration: $SESSION_H ($SESSION_DURATION s)
since_last_user: $SINCE_LAST_H ($SINCE_LAST s)
ledger: $LEDGER$IDLE_WARN"
emit_additional_context UserPromptSubmit "$CTX"
exit 0
# chronos — SessionStart hook (PowerShell)
param([string]$JsonInput = '')
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot '_lib.ps1')
$RawInput = if ($JsonInput) { $JsonInput } else { [Console]::In.ReadToEnd() }
$Session = Resolve-SessionId -Input $RawInput
$Ledger = Get-LedgerPath -Session $Session
$State = Get-StatePath -Session $Session
$NowUtc = Get-NowUtcIso
$NowLocal = Get-NowLocalIso
$NowEpoch = Get-NowEpoch
$Tz = Get-TzName
$Offset = Get-UtcOffset
# Reset ledger
Set-Content -Path $Ledger -Value '' -NoNewline
$StateObj = @{
session_id = $Session
started_at_utc = $NowUtc
started_at_local = $NowLocal
started_at_epoch = $NowEpoch
tz = $Tz
utc_offset = $Offset
turn = 0
last_user_at_utc = $NowUtc
last_user_at_epoch = $NowEpoch
}
$StateObj | ConvertTo-Json -Compress | Set-Content -Path $State -NoNewline
try { Invoke-LedgerCleanup } catch {}
Set-Content -Path (Join-Path $script:ChronosHome 'current-session') -Value $Session -NoNewline
$Ctx = @"
chronos baseline
now_utc: $NowUtc
now_local: $NowLocal
tz: $Tz ($Offset)
ledger: $Ledger
state: $State
session: $Session
Before reasoning about 'when' or 'how long ago', consult the ledger or run ``date -u``. Follow chronos/SKILL.md decision rules.
"@
Write-AdditionalContext -Event 'SessionStart' -Context $Ctx
exit 0
#!/usr/bin/env bash
# chronos — SessionStart hook
# Reads hook stdin JSON, initializes ledger + state, emits baseline context.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$SCRIPT_DIR/_lib.sh"
INPUT=$(cat)
SESSION=$(resolve_session_id "$INPUT")
LEDGER=$(ledger_path "$SESSION")
STATE=$(state_path "$SESSION")
NOW_UTC=$(now_utc_iso)
NOW_LOCAL=$(now_local_iso)
NOW_EPOCH=$(now_epoch)
TZ=$(tz_name)
OFFSET=$(utc_offset)
# Init ledger (truncate if fresh session start)
: > "$LEDGER"
# Init state
if command -v jq >/dev/null 2>&1; then
jq -n \
--arg s "$SESSION" --arg iso "$NOW_UTC" --arg loc "$NOW_LOCAL" \
--arg tz "$TZ" --arg off "$OFFSET" --argjson ep "$NOW_EPOCH" \
'{session_id:$s, started_at_utc:$iso, started_at_local:$loc, started_at_epoch:$ep, tz:$tz, utc_offset:$off, turn:0, last_user_at_utc:$iso, last_user_at_epoch:$ep}' \
> "$STATE"
else
cat > "$STATE" <<EOF
{"session_id":"$SESSION","started_at_utc":"$NOW_UTC","started_at_local":"$NOW_LOCAL","started_at_epoch":$NOW_EPOCH,"tz":"$TZ","utc_offset":"$OFFSET","turn":0,"last_user_at_utc":"$NOW_UTC","last_user_at_epoch":$NOW_EPOCH}
EOF
fi
# Opportunistic cleanup of old ledgers
cleanup_ledgers 2>/dev/null || true
# Persist current session ID for platforms without stdin session_id
echo "$SESSION" > "$CHRONOS_HOME/current-session"
CTX="chronos baseline
now_utc: $NOW_UTC
now_local: $NOW_LOCAL
tz: $TZ ($OFFSET)
ledger: $LEDGER
state: $STATE
session: $SESSION
Before reasoning about 'when' or 'how long ago', consult the ledger or run \`date -u\`. Follow chronos/SKILL.md decision rules."
emit_additional_context SessionStart "$CTX"
exit 0
# chronos — Stop hook (PowerShell)
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot '_lib.ps1')
$Input = [Console]::In.ReadToEnd()
$Session = Resolve-SessionId -Input $Input
$State = Get-StatePath -Session $Session
$NowEpoch = Get-NowEpoch
if (-not (Test-Path $State)) { exit 0 }
$S = Get-Content $State -Raw | ConvertFrom-Json
$SinceLast = $NowEpoch - [int]$S.last_user_at_epoch
if ($SinceLast -gt $script:IdleThresholdSec) {
[Console]::Error.WriteLine("chronos: stop after idle=${SinceLast}s (threshold=$($script:IdleThresholdSec)s). If autonomous, consider escalation.")
}
exit 0
#!/usr/bin/env bash
# chronos — Stop hook. Checks idle status for autonomous loops.
# Exit 0 = normal. Chronos never blocks Stop — only surfaces warnings to stderr (agent may see in transcript).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$SCRIPT_DIR/_lib.sh"
INPUT=$(cat)
SESSION=$(resolve_session_id "$INPUT")
STATE=$(state_path "$SESSION")
NOW_EPOCH=$(now_epoch)
if [ ! -f "$STATE" ]; then exit 0; fi
LAST_USER_EPOCH=0
if command -v jq >/dev/null 2>&1; then
LAST_USER_EPOCH=$(jq -r '.last_user_at_epoch // 0' "$STATE")
fi
SINCE_LAST=$(( NOW_EPOCH - LAST_USER_EPOCH ))
# If idle > threshold and agent is stopping, emit a soft notice (exit 0).
if [ "$SINCE_LAST" -gt "$CHRONOS_IDLE_THRESHOLD_SEC" ]; then
echo "chronos: stop after idle=${SINCE_LAST}s (threshold=${CHRONOS_IDLE_THRESHOLD_SEC}s). If autonomous, consider escalation." >&2
fi
exit 0