Skip to content
histori-devPublic

About

A local-first circuit breaker for Claude Code — catches your agent when it gets stuck and pulls the brakes. Zero deps, never phones home.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

histori

A local-first circuit breaker for Claude Code. Catches your agent when it gets stuck and pulls the brakes.

histori is a small set of Claude Code hooks that watch the tool calls and transcript of a session and look for the shapes that show up when an agent is spinning its wheels — repeating the same edit, re-running the same failing test, thrashing without landing progress, burning an unreasonable number of calls or tokens in one turn, or losing the thread of a long conversation. When it recognizes one of those patterns it either warns you or, if you've told it to, denies the next tool call outright so the agent has to stop and re-evaluate.

histori braking a looping Claude Code agent

Re-enactment of a real verified session — reproduce it yourself with demo/loop-task.md.

Quick start

# Install into this repo, warn mode (default) — histori tells you when
# it sees a loop, but never blocks a tool call
npx @histori-dev/histori init

# Install into this repo, brake mode — histori denies the next tool
# call when it detects a loop, forcing the agent to stop
npx @histori-dev/histori init --unattended

# See what histori has caught
npx @histori-dev/histori log

# See or set the session's goal anchor
npx @histori-dev/histori goal
npx @histori-dev/histori goal "build the v2 export pipeline"

# Quick health check: goal anchor, incident count, last session's token burn
npx @histori-dev/histori status

# per-session audit: tokens, est. cost, incidents
npx @histori-dev/histori sessions

# Remove the hooks
npx @histori-dev/histori uninstall

By default init/uninstall write to .claude/settings.json in the current repo. Pass --user to install into ~/.claude/settings.json instead, so it applies to every project:

npx @histori-dev/histori init --user --unattended

histori only ever edits its own entries in settings.json (tagged internally so it can find and remove exactly what it added) — any hooks you already have configured are left alone. If settings.json exists but isn't valid JSON, histori refuses to touch it and prints an error instead of guessing.

Install as a Claude Code plugin

/plugin marketplace add histori-dev/histori
/plugin install histori@histori

What you get: the hooks auto-register while the plugin is enabled (toggle it on/off any time with /plugin), plus three in-session slash commands — /histori:status, /histori:goal, and /histori:log.

Previously installed via npx? Run npx @histori-dev/histori uninstall. If both are present, the plugin automatically stands down (settings.json wins) so nothing double-counts — /histori:status will warn you.

Contributing: dist/ is committed to this repo because plugins install by git-clone with no build step — run npm run build and commit dist/ alongside any src/ changes.

Codex CLI

histori also works with Codex CLI, via Codex's own hooks mechanism.

# Install into this repo (writes .codex/hooks.json), warn mode by default
npx @histori-dev/histori init --harness codex

# Brake mode
npx @histori-dev/histori init --harness codex --unattended

# Remove the hooks
npx @histori-dev/histori uninstall --harness codex

Codex requires a one-time hook trust step before it will run them: open Codex, run /hooks, and choose "Trust all and continue". For headless/non-interactive setups, pass --dangerously-bypass-hook-trust to Codex instead.

Install as a Codex plugin

This repo is also packaged as an installable Codex plugin (.codex-plugin/plugin.json, hooks/codex-hooks.json) — point Codex's plugin install flow at this repository to register histori's hooks for every session without editing .codex/hooks.json by hand.

Honest scope on Codex

  • On Codex, GPT-family models show token counts but no USD estimate yet (histori's price table currently covers Anthropic models; tokens are always counted).
  • Codex hosted tools (like WebSearch) don't pass through the local hook path, so histori can't see them; everything local — shell, file edits via apply_patch, MCP tools — is covered.
  • Codex treats hooks as a guardrail, not a hard security boundary, and so should you.
  • Codex currently doesn't report failed shell commands back to hooks, so the repeated-failure detector (D2) mostly defers to the repeat detector (D1) there — a loop of identical failing commands still gets caught.
  • For the same reason, D3's no-progress counter also can't see failed shell commands on Codex (they never reach PostToolUse), so a loop of distinct failing commands is backstopped by D4's runaway-call limit on Codex, rather than D3.

What it catches

Detection runs on every tool call. In warn mode (the default from histori init) a match prints a message and lets the call through. In brake mode (histori init --unattended) a match denies the call.

Detector Catches Default trigger (warn / brake)
D1 — repeat The exact same tool call (same tool, same command/file) fired over and over 5× / 4× within the recent-call window
D2 — repeated failure The same action failing again and again — retrying isn't working 4× / 3× failures
D3 — no-progress thrash A run of tool calls with nothing landing (no successful edit/write in between) 15 calls / 10 calls since the last edit
D4 — runaway A single turn blows through a call-count or wall-clock budget 40 calls or 15 min / 30 calls or 10 min
D5 — token budget The session or a single turn burns through more tokens than the budget allows 1.5M session / 300k turn (warn) / 3M session / 500k turn (brake)
D6 — thinking dominance* A turn that was almost all reasoning and barely any action — a pure-thinking loop ≥90% thinking with ≥20k thinking chars (same threshold in both presets)

Every triggered detection is appended to a local incident log (.histori/incidents.jsonl in the target repo) so histori log can show you a history of what got caught and when.

* D6 is warn-only in both presets — see Limitations below for why it never brakes.

How budgets count (D5)

D5 sums input_tokens + output_tokens straight from the usage field Claude Code writes into the transcript for each assistant turn — the same numbers the API bills you for. Cache reads are deliberately excluded. A long session resends the same system prompt, tool results, and file contents as cached context on almost every turn; if those cache-read tokens counted toward the budget, the number would mostly track how long the conversation has been open rather than how much new work is happening, and the budget would blow past its ceiling on any long-but-healthy session. Counting only input+output keeps D5 tracking genuinely new tokens — new instructions in, new reasoning and output out — which is what you actually want a runaway-cost tripwire to watch.

Before the hard ceiling, there's a soft zone: at budget.softPct% of the session budget (default 70%, set 0 to disable) histori fires a one-time advisory warning — "consider wrapping up" — even in brake mode, so you get a heads-up before anything actually gets denied. It's a nudge, not a brake: the hard ceiling still brakes exactly as before once you cross it.

histori also shows an estimated USD cost (~$X.XX est.) alongside the token counts, computed from a static list-price table for Anthropic model families (opus/sonnet/haiku) applied to input/output tokens per turn. Unknown or non-Anthropic models show tokens only, no cost. Cache reads are excluded from the estimate, same as the token budget itself. These numbers are informational — pricing changes, the table can drift, and tokens remain the actual unit the budget enforces.

Goal anchor (context rot)

The first substantive prompt in a session (≥20 characters, not a slash command) is captured verbatim — after scrubbing it for secret-looking tokens and keys (API keys, bearer tokens, passwords) — and written to .histori/goal.md in the target repo. That file is yours: edit it any time to correct or sharpen the anchor, or delete it — but note the next substantive prompt will capture a fresh one. To turn anchoring off for good, set "anchor": { "enabled": false } in .histori/config.json.

Every anchor.everyNPrompts prompts (default 5), histori re-injects the goal as additional context (verbatim up to 1,000 characters; longer goals are truncated on injection, the full text stays in the file) on UserPromptSubmit, so it stays in the model's context even as the conversation grows. It's also always re-injected on the first prompt after a detected compaction — a summary boundary in the transcript or a PreCompact hook firing — since that's exactly the moment a session is most likely to lose track of what it was originally asked to do.

Check or set the anchor from the CLI:

npx @histori-dev/histori goal              # print the current goal (or a hint if none is set)
npx @histori-dev/histori goal "your goal"  # set/overwrite it
npx @histori-dev/histori status             # goal anchor state + incident count + last session's token burn

Configuration

histori reads .histori/config.json in the target repo (or ~/.histori/config.json for --user installs). Any field you omit falls back to the preset (assisted unless preset: "unattended" is set):

{
  "preset": "assisted",
  "mode": "warn",
  "thresholds": {
    "repeatCount": 5,
    "repeatWindow": 8,
    "failCount": 4,
    "noProgress": 15,
    "runawayCalls": 40,
    "runawayMs": 900000
  },
  "budget": {
    "sessionTokens": 1500000,
    "turnTokens": 300000,
    "softPct": 70
  },
  "anchor": {
    "enabled": true,
    "everyNPrompts": 5
  },
  "enabled": { "D1": true, "D2": true, "D3": true, "D4": true, "D5": true, "D6": true }
}

What's coming next

histori is deliberately narrow: it looks at the shape of tool-call traffic and transcript usage (repeats, failures, progress, time/volume, tokens, thinking ratio), not the meaning of what the agent is doing. It does not yet catch, and does not claim to catch:

  • Semantic goal-drift — the agent is technically making progress, but on the wrong idea (e.g. locked onto a fix that can't work, or refactoring something that was never broken). The goal anchor mitigates this by keeping the original ask in context; it doesn't detect drift itself.
  • Live thinking-loop interruption — D6 catches a pure-thinking loop only after the fact, at the end of the turn (see Limitations).
  • Cross-harness support — histori hooks into Claude Code and Codex CLI (see Codex CLI above, including its honest-scope notes). Cursor and Gemini CLI adapters are not implemented.

If you need any of the above today, histori isn't there yet — it's a tripwire for the mechanical failure modes, not a judgment layer.

Limitations

  • D6 is retroactive, not live. It's evaluated once, at Stop/SubagentStop, over the turn that just finished. A turn that's a pure thinking loop fires no hooks while it's happening — Claude Code hooks only run around tool calls and turn boundaries, so there's nothing for histori to observe mid-turn. That means D6 can tell you after the fact that a turn burned 40k characters of thinking with nothing to show for it, but it can't interrupt that turn while it's still running. Doing that would require a daemon watching the transcript file in real time, independent of the hook lifecycle — out of scope for v2.
  • The transcript format is undocumented and internal to Claude Code. D5, D6, and the goal anchor all read Claude Code's JSONL transcript files (token usage, thinking blocks, compaction markers) — a format Anthropic hasn't committed to keeping stable. If it changes, these v2 features degrade silently (parsing failures are swallowed and treated as zero, per histori's fail-open design) rather than crashing or blocking your agent. The v1 detectors (D1–D4) don't depend on the transcript at all, so they're unaffected either way.

Local-first, zero dependencies, never phones home

  • No network calls anywhere in the codebase — histori doesn't ping a server, doesn't check for updates, doesn't report telemetry. You can verify this yourself; there isn't a single fetch/http call in src/.
  • Zero runtime dependencies. The installed package is just the CLI and hook scripts.
  • All state lives in the target repo under .histori/ (session state, config, incident log) — nothing leaves your machine, and nothing is stored outside the project you ran init in (or ~/.claude for --user installs).
  • Everything fails open: if a hook errors, can't parse its input, or hits a malformed config, histori lets the tool call through rather than stalling your agent.

Uninstalling

histori uninstall          # remove from this repo's .claude/settings.json
histori uninstall --user   # remove from ~/.claude/settings.json

This only strips the hook entries histori added; it doesn't touch .histori/ (delete that directory yourself if you want to clear state/logs) and doesn't touch any other hooks in settings.json.

License

MIT — see LICENSE.

About

A local-first circuit breaker for Claude Code — catches your agent when it gets stuck and pulls the brakes. Zero deps, never phones home.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages