🧠 Hunch docs
Documentation

Hunch — Engineering Memory OS

Git stores what the code is. Hunch stores why — a persistent, git-native reasoning graph of decisions, bugs, and invariants, surfaced to your coding assistant at reasoning time so it stops re-deriving understanding and stops undoing intentional design.

Hunch is a CLI plus a local MCP server. It captures the “why” as a byproduct of normal work — commits and test failures — stores it as git-tracked JSON in .hunch/, and feeds the minimal relevant slice back to any MCP assistant (Claude Code, Cursor, Copilot, Windsurf, Codex). Local-first, no SaaS, and synthesis is billed to your existing coding-assistant subscription — never a pay-per-token API key.

In a hurry? npm i -g @davesheffer/hunchcd your-repo && hunch init && hunch backfill --since 90d → reload your assistant and ask “why is X built this way?”. The rest of this page is the full tour.

Install#

Hunch needs Node ≥ 20. Install the CLI globally — that puts a hunch binary on your PATH:

terminal
npm install -g @davesheffer/hunch

Or run from source (for hacking on Hunch itself):

terminal
git clone https://github.com/davesheffer/hunch && cd hunch
npm install
npm run build        # compile to dist/
npm link             # optional: global `hunch` from source

Initialize a repo#

Run hunch init in the repo you want a memory for. One command scaffolds everything:

terminal
hunch init               # scaffold .hunch/, index, install hooks, wire up assistants
hunch backfill --since 90d # cold start: seed decisions from recent git history

hunch init does the following, idempotently (re-running is safe and merges into existing files):

  • Scaffolds .hunch/ and indexes the repo into a symbol/dependency graph.
  • Installs a post-commit hook (the learning loop) and a pre-commit Constraint Guard.
  • Writes .mcp.json + slash commands + an auto-maintained CLAUDE.md, and registers a git merge driver for the graph.
  • Wires up every detected assistant — Claude Code, Cursor, VS Code (Copilot), Windsurf, Codex — to the same .hunch/ graph.
init writes a .mcp.json pointing at this machine’s Node + Hunch, so each teammate runs hunch init once to wire up their own clone. The captured .hunch/ content is shared via git.

Your first query#

Ask the codebase why — from the CLI, or in your assistant once the MCP server is registered:

terminal
hunch why src/auth/session.ts   # decisions / bugs / invariants behind a file
hunch doctor                    # git, schema version, synthesis mode

…and in your assistant: “why is the session module built this way?” — Claude consults Hunch and answers with citations (record ids + provenance).

Synthesis & billing#

When Hunch turns a commit into a structured Decision, the LLM call is billed to your coding-assistant subscription through its CLI — never a pay-per-token API key (the API key is stripped from the spawned environment). Hunch auto-detects the first CLI present, in order:

CLISubscriptionDetected by
claude (Claude Code)Claude Pro / Maxclaude --version
codex (OpenAI Codex)ChatGPT Plus / Procodex --version
cursor-agent (Cursor)Cursorcursor-agent --version

If none is installed, Hunch still works with a deterministic structural heuristic (lower-confidence drafts). hunch doctor tells you which mode you’re in; force one with the HUNCH_SYNTH_PROVIDER env var:

terminal
export HUNCH_SYNTH_PROVIDER=claude-cli   # | codex-cli | cursor-agent | deterministic

Deep Synthesis v0.19. --deep reconciles multiple independent drafts into one: across every signed-in assistant CLI, or — with a single CLI — by sampling it N times for self-consistency (--samples, default 2). The richest draft is the spine, alternatives & consequences are unioned, and confidence is agreement-weighted (more independent agreement → higher score, capped below the enforcement threshold so it stays advisory). Add --verify (automatic under --deep) for a Critic pass that audits each draft against its commit — pruning unsupported rejected-alternatives before they become tripwires and down-weighting weak grounding; it only ever lowers confidence, never arming enforcement. Slower; subscription-only; never on the guard path; degrades to the single-provider draft when no CLI is available.

terminal
hunch backfill --deep        # ensemble / self-consistency across history
hunch sync <sha> --deep       # reconcile a single commit (verify on)
hunch sync <sha> --verify     # Critic pass only — audit + prune, no ensemble

MCP & assistants#

The Hunch MCP server is client-agnostic: one .hunch/ graph powers every assistant. hunch init scaffolds each tool’s MCP config plus an ambient grounding file so they all consult the same memory.

AssistantMCP configGrounding file
Claude Code.mcp.jsonCLAUDE.md + /hunch-* commands
Cursor.cursor/mcp.json.cursor/rules/hunch.mdc
VS Code (Copilot).vscode/mcp.json.github/copilot-instructions.md
Windsurf.windsurf/mcp_config.json.windsurf/rules/hunch.md
Codex / any.codex/config.tomlAGENTS.md

The server runs over stdio and exposes the hunch_* tools. You can start it manually to verify:

terminal
hunch mcp   # serves Hunch over stdio (this is what .mcp.json launches)
Prefer hunch init over a global claude mcp add. .mcp.json is registered by file path, so it’s robust and travels with the repo.

Reload your assistant#

After hunch init (or after pulling new Hunch versions), the MCP server config has changed but a running assistant session still holds the old process. Reload your assistant in the repo — reload the window / restart the session — so it respawns the server and picks up the hunch_* tools.

The on-disk directory is .hunch/ and the tools are hunch_*. If the tools don’t appear after a reload, run hunch doctor.

Private memory v0.17#

Open-source your code without open-sourcing your reasoning. Keep sensitive decisions/bugs/constraints in a separate private location and Hunch unions it into every query and guard locally — your MCP tools and the pre-edit hook see it — while your public .hunch/ stays clean.

One command sets it up (no env var, no shell-profile edit):

terminal
hunch private                       # default ./.hunch-private (gitignored)
hunch private ~/work/secret-memory   # or a path you choose
hunch private --repo git@github.com:you/mem-private.git   # or clone a private repo

It writes a gitignored .hunch/local.json pointing at the store, installs the --private post-commit hook, and that’s it — Hunch (and the MCP server) auto-detect it; hunch doctor shows private: on → …. It’s opt-in and default-off: no local.json (and no HUNCH_PRIVATE_DIR) means the overlay is completely inert.

Record sensitive items with private: true on hunch_record_decision / hunch_record_correction; they’re written to the overlay, never the committed repo. Post-commit synthesis routes its auto-captured decisions there too, so a private-memory repo’s auto-decisions never land in the public tree. HUNCH_PRIVATE_DIR overrides local.json per-shell (e.g. for portability); the git hook lives in .git/hooks/ and is never committed.

Auto-commit (opt-in). Add --auto-commit to hunch private (or hunch init for a regular repo) and the post-commit hook also git add + commit + pushes each captured decision to the repo it lands in — the private store under a private overlay, else this repo. It stages only the .hunch/ dir (never your other changes) and is recursion-safe. Off by default.

Leak-safe by construction. Committed files (the auto CLAUDE.md grounding) and the CI PR comment render public-only — a private record can’t reach a public surface. A private constraint is still enforced on your machine (pre-edit hook + local hunch check), just never posted. Keep the var unset in CI; the scaffolded guard also passes --public-only and neutralizes it as defense-in-depth.

Windows note#

On Windows, a global claude mcp add writes to ~/.claude.json keyed by the raw working-directory string. Drive letters are case-insensitive (c:\ and C:\ are the same folder) but Claude Code compares the key case-sensitively — so it can create two project blocks for one directory, and a session that resolves to the other casing sees no hunch_* tools.

If you hit this, run hunch doctor — on Windows it detects the split and heals it (merging the MCP servers across both casings, after backing up ~/.claude.json). hunch init runs the same heal automatically.

The reasoning graph#

Hunch stores four kinds of record as git-tracked JSON, plus the code graph that links them:

RecordWhat it captures
DecisionWhy a change was made — an ADR auto-drafted from each commit (context, decision, alternatives, consequences).
BugA root cause + lineage (introduced → fixed → recurred), captured from a failing test with a ranked suspect list.
ConstraintA do-not-break invariant. Recurring or severe bugs get promoted into one; corrections mint one directly.
Component / Symbol / EdgeThe tree-sitter code graph — functions, call edges, imports, components — plus churn and fan-in metrics.

The flow is events → extract → synthesize → store → ground. The JSON in .hunch/ is the source of truth; a local SQLite file is a derived FTS + vector index, rebuilt on demand and never authoritative.

Provenance & confidence#

Every record carries provenance — source + confidence + evidence — so nothing is a blind assertion. Sources rank inferred < llm_draft < human_confirmed. Low-confidence auto-drafts stay advisory and are cheap to discard; the enforcement gates only ever block on high-confidence, human-vouched records (see firmness). Triage drafts with hunch review.

Time-travel#

Decisions and constraints carry a git-anchored valid-time window. Supersession closes a window instead of deleting the record, so history stays auditable. Ask the graph what was believed at any past commit or tag:

terminal
hunch why src/auth/session.ts --as-of v0.7.0
hunch timeline src/auth/session.ts   # the full decision history, newest first

The hunch_why and hunch_context MCP tools take the same optional as_of ref.

MCP tools#

The tools your assistant calls under the hood. Read tools answer “why / what-must-not-break / blast radius”; write tools persist new memory.

ToolWhat it does
hunch_queryFull-text + graph search across all records, ranked with provenance.
hunch_whyDecisions, bugs & constraints behind a file/symbol (accepts as_of).
hunch_check_constraintsInvariants whose scope matches a path/glob — call before editing.
hunch_get_dependentsTransitive dependents of a symbol — the blast radius.
hunch_blast_radiusDependent files + near-violations a change could break indirectly.
hunch_bug_lineageBugs matching a symptom/symbol with their introduced→fixed→recurred lineage.
hunch_contextThe minimal relevant slice for a task — invariants, why, bug history, blast radius.
hunch_timelineA target’s decision history over time, with valid-time windows.
hunch_merge_verdictA cited BLOCK / WARN / PASS over a diff (see below).
hunch_record_decisionWrite-back: persist a new Decision (ADR) with provenance.
hunch_record_correctionWrite-back: turn a correction into an enforced invariant (see Never Twice).

Slash commands#

For Claude Code, hunch init also writes ergonomic slash commands:

CommandWhat it does
/hunch-why <file|symbol>The decisions, invariants, and bug history behind it — with citations.
/hunch-fix <bug>Fix a bug grounded in past root causes, blast radius, and constraints.
/hunch-fragileA fragility report — the riskiest code, with evidence.

Firmness levels#

Telling an assistant “consult Hunch first” in a prompt drifts. Instead, hunch init installs agent hooks so grounding is enforced by the harness. How hard it pushes is one committed knob — set it once, it applies to the whole team:

terminal
hunch firmness          # print the current level
hunch firmness strict   # change it (takes effect on the next edit; no restart)
LevelBefore an edit
offnothing (the hook is a no-op)
advisory defaultinject the relevant Hunch slice as context
firmadvisory + explicitly flag invariants in the file’s scope
strictfirm + deny an edit that hits a blocking invariant (directly or via blast radius), feeding the invariant back as the refusal reason
The hook never breaks your flow: any error or unrecognized input emits nothing and exits 0, and it stays silent on files Hunch hasn’t learned. strict only bites once you have blocking constraints recorded.

Never Twice v0.13#

The most expensive failure in AI coding is being corrected and then re-corrected: you say “no, never call X here”, the agent complies once, and next session it does it again — because the feedback was stored as advisory text, not enforced. Hunch closes that loop.

When you correct the agent, it captures the rule as a first-class Constraint (provenance human_confirmed) via hunch_record_correction — and from then on the same pre-edit hook + CI guard hold every assistant to it.

in your assistant
You:   "no — never import lodash, we ship our own utils"
Agent: hunch_record_correction({ rule: "never import lodash; use src/utils",
                                 scope_hint_file: "src/cart.ts", severity: "blocking" })
 con_… recorded. A later edit that adds `import _ from "lodash"` to that scope is
  DENIED (strict) and the PR fails CI — in Cursor, Copilot, Windsurf, or Claude Code alike.

The UserPromptSubmit hook nudges the agent to persist a rule whenever your prompt reads like a correction. Scoping is conservative by default (the file you were in); a repo-wide rule is only blocking when you pass applies_to_all, so one correction can’t silently gate the whole tree.

Causal Merge Verdict v0.14#

A diff-only reviewer sees what changed. It can’t see that the line you’re deleting is the fix for an incident, that the symbol you’re re-adding was deliberately retired, or that the helper you’re adding already exists three modules over. Hunch can — because it holds the why and the whole graph.

hunch_merge_verdict (and hunch check / the CI guard) replays a diff against the graph and returns one cited verdict — BLOCK / WARN / PASS — that shows the reasoning, not just the rule:

hunch_merge_verdict
VERDICT: ⛔ BLOCK — this change breaks a recorded invariant or re-opens a known bug.

⛔ pay() must verify the session before charging — con_pay
   🧠 why: "Charge must verify the session first" (dec_pay)
   🐞 guards against: Double-charge on unverified session — pay() charged without verifying (bug_…)

It’s deterministic (no LLM): for every invariant directly in scope it walks constraint → source_decision → the bug whose root cause spawned it. BLOCK fires only on a direct, high-confidence, non-stale blocking invariant or a blocking-linked regression — near-hits stay advisory, so it’s safe as a merge gate. Call it with {} to check staged changes, or pass base: "origin/main" for a PR range.

Decision Guard (Veto) v0.15#

Decisions record not just what you chose but the alternatives you rejected. The Decision Guard flags a change that reverses a decision — re-introducing an approach an in-force decision rejected — so the agent can’t quietly walk back a deliberate call.

terminal
hunch veto              # flag changes that reverse a rejected alternative

Like the other gates: it warns by default and blocks under strict only when the rejected-alternative is human-confirmed, in-force, and non-stale. Machine-checkable “tripwires” can be drafted for existing rejected alternatives and confirmed with hunch review --accept <id> to enable blocking.

Regression Guard#

Re-adding a symbol or dependency that an in-force decision deliberately retired gets flagged — warns always, fails a strict commit only when the retirement is tied to a blocking invariant. The agent stops undoing intentional design. The pre-edit hook also surfaces what a decision retired from a file (“don’t re-add login here — dec_017 removed it”), and the commit-time hunch check does the actual gating.

Redundancy Guard v0.16#

Coding agents work from a local context window, so they re-implement a helper that already lives three modules over, or re-add a dependency the codebase already has — sprawl a diff-only reviewer can’t see. Hunch holds the whole symbol graph, so a diff that adds a function or class already defined elsewhere is flagged with its existing location.

hunch check
Possibly re-implements 1 symbol(s) that already exist (advisory — review, never blocks):

  ⟲ adds function `formatDate` — already defined in src/util/date.ts

It’s deterministic and advisory — it never blocks a commit or fails --strict; it surfaces in hunch check, the CI guard, and hunch_merge_verdict alongside the other dimensions. Tuned to stay quiet: stopword + length filters, scoped to the change’s own top-level project root (so src/ never trips over test/ or a sub-project), and move-aware — a refactor that relocates a symbol isn’t mistaken for a duplicate.

CI Constraint Guard#

Memory that blocks a merge, not just advises. hunch ci scaffolds a GitHub Action that runs hunch check on every PR, comments the affected invariants and decisions (with con_/dec_ ids and the causal why), and fails the check on a direct, high-confidence, non-stale blocking invariant.

terminal
hunch ci    # scaffold .github/workflows/ + the PR comment action
run a check locally
hunch check --staged --strict      # gate staged changes
hunch check --base origin/main      # gate a PR range (used in CI)

CLI reference#

Run hunch <command> --help for full flags. The everyday commands:

CommandWhat it does
initScaffold .hunch/, index, install hooks, wire up assistants.
indexParse the repo into a symbol/dependency graph (deterministic, no LLM).
backfillReplay git history to seed decisions (cold-start fix). --since 90d
syncCapture a decision from a commit (run by the post-commit hook).
queryFull-text + graph search. Add --semantic for embeddings recall.
whyExplain why a file/symbol is the way it is. --as-of <ref>
timelineThe decision history for a file/symbol over time.
contextAssemble the minimal relevant Hunch slice for a task.
fragileRanked fragility report with evidence.
checkFlag changes that touch a do-not-break invariant (local + CI guard); also flags (advisory) symbols you add that already exist elsewhere.
vetoDecision Guard — flag changes that reverse a rejected alternative.
ciScaffold the CI Constraint Guard (GitHub Action).
record-constraintRecord an invariant the codebase must not break.
record-bugCapture a Bug from a failing test (symptom + suspects).
testRun tests; capture failures as Bugs, promote recurrences to Constraints.
supersedeMark one decision as replaced by another (invalidate, don’t delete).
staleList decisions/constraints whose files changed after last verified (drift).
reviewTriage low-confidence drafts: list, accept, or reject.
firmnessGet or set how firmly the agent hook enforces Hunch.
embedGenerate local embeddings for semantic search (opt-in).
compactPrune low-value auto-captured records.
migrateUpgrade .hunch/ records to the current schema version.
mcpStart the MCP server over stdio.
doctorDiagnose the environment (git, synthesis provider, index freshness).

Where it’s stored#

Everything lives under .hunch/ as git-tracked JSON — the source of truth, diffable and reviewable in PRs:

.hunch/
.hunch/
├─ decisions/      one JSON file per Decision (ADR)
├─ bugs/           root causes + lineage
├─ constraints/    do-not-break invariants
├─ components/     code components + fragility
├─ symbols/  edges/  the tree-sitter graph (index.json)
├─ manifest.json   on-disk schema version
├─ config.json     firmness + runtime config
└─ hunch.sqlite    DERIVED FTS+vector index (gitignored; rebuilt by `hunch index`)
The SQLite index is gitignored and regenerable. Only the JSON is committed — so the graph syncs for free over git push / pull.

Doctor & troubleshooting#

hunch doctor is the first stop for anything that looks off — it reports git status, the resolved synthesis provider, schema version, index freshness, and record counts (and on Windows, heals the ~/.claude.json casing split).

terminal
hunch doctor
SymptomFix
Assistant shows no hunch_* toolsReload the assistant in the repo; if still missing, hunch doctor.
Synthesis is deterministicLog into a coding-assistant CLI (e.g. claude /login); drafts become high-confidence.
Graph looks stalehunch index to rebuild the symbol graph from current source.
Schema mismatch after upgradehunch migrate to upgrade .hunch/ records.

Working as a team#

The .hunch/ JSON is the source of truth — diffable, reviewable in PRs, synced for free over git push / pull. hunch init registers a git merge driver so concurrent edits to the graph merge by record id instead of throwing conflict markers (human-confirmed beats auto, then higher confidence, then recency). The graph is OS-agnostic: paths are stored in POSIX form, and an installed Hunch registers its MCP server by package name — so teammates on Windows, macOS, or Linux share the same memory without per-machine fixups.

Maintenance#

Two housekeeping commands keep the graph healthy over time:

terminal
hunch compact   # prune rejected/superseded/stale low-value drafts
hunch stale     # list records whose files changed after they were last verified

Use hunch review to triage low-confidence drafts before they accumulate — accept the good ones (promoting them to higher confidence) and reject the noise.

Develop & contribute#

Hunch is pure TypeScript ESM, Node ≥ 20, no build step at dev time (runs via tsx). It’s licensed Apache-2.0.

terminal
npm run dev -- <args>   # run the CLI from source via tsx
npm run typecheck       # strict tsc — the gate (no separate lint)
npm test                # tsx --test over test/*.test.ts
npm run build           # compile to dist/ (the published artifact)

See CONTRIBUTING.md for the contribution flow.