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.
npm i -g @davesheffer/hunch → cd 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:
npm install -g @davesheffer/hunchOr run from source (for hacking on Hunch itself):
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:
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-maintainedCLAUDE.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:
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:
| CLI | Subscription | Detected by |
|---|---|---|
claude (Claude Code) | Claude Pro / Max | claude --version |
codex (OpenAI Codex) | ChatGPT Plus / Pro | codex --version |
cursor-agent (Cursor) | Cursor | cursor-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:
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.
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.
| Assistant | MCP config | Grounding file |
|---|---|---|
| Claude Code | .mcp.json | CLAUDE.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.toml | AGENTS.md |
The server runs over stdio and exposes the hunch_* tools. You can start it manually to verify:
hunch mcp # serves Hunch over stdio (this is what .mcp.json launches)
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.
.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):
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.
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:
| Record | What it captures |
|---|---|
| Decision | Why a change was made — an ADR auto-drafted from each commit (context, decision, alternatives, consequences). |
| Bug | A root cause + lineage (introduced → fixed → recurred), captured from a failing test with a ranked suspect list. |
| Constraint | A do-not-break invariant. Recurring or severe bugs get promoted into one; corrections mint one directly. |
| Component / Symbol / Edge | The 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:
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.
| Tool | What it does |
|---|---|
hunch_query | Full-text + graph search across all records, ranked with provenance. |
hunch_why | Decisions, bugs & constraints behind a file/symbol (accepts as_of). |
hunch_check_constraints | Invariants whose scope matches a path/glob — call before editing. |
hunch_get_dependents | Transitive dependents of a symbol — the blast radius. |
hunch_blast_radius | Dependent files + near-violations a change could break indirectly. |
hunch_bug_lineage | Bugs matching a symptom/symbol with their introduced→fixed→recurred lineage. |
hunch_context | The minimal relevant slice for a task — invariants, why, bug history, blast radius. |
hunch_timeline | A target’s decision history over time, with valid-time windows. |
hunch_merge_verdict | A cited BLOCK / WARN / PASS over a diff (see below). |
hunch_record_decision | Write-back: persist a new Decision (ADR) with provenance. |
hunch_record_correction | Write-back: turn a correction into an enforced invariant (see Never Twice). |
Slash commands#
For Claude Code, hunch init also writes ergonomic slash commands:
| Command | What 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-fragile | A 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:
hunch firmness # print the current level hunch firmness strict # change it (takes effect on the next edit; no restart)
| Level | Before an edit |
|---|---|
off | nothing (the hook is a no-op) |
advisory default | inject the relevant Hunch slice as context |
firm | advisory + explicitly flag invariants in the file’s scope |
strict | firm + deny an edit that hits a blocking invariant (directly or via blast radius), feeding the invariant back as the refusal reason |
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.
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:
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.
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.
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.
hunch ci # scaffold .github/workflows/ + the PR comment action
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:
| Command | What it does |
|---|---|
init | Scaffold .hunch/, index, install hooks, wire up assistants. |
index | Parse the repo into a symbol/dependency graph (deterministic, no LLM). |
backfill | Replay git history to seed decisions (cold-start fix). --since 90d |
sync | Capture a decision from a commit (run by the post-commit hook). |
query | Full-text + graph search. Add --semantic for embeddings recall. |
why | Explain why a file/symbol is the way it is. --as-of <ref> |
timeline | The decision history for a file/symbol over time. |
context | Assemble the minimal relevant Hunch slice for a task. |
fragile | Ranked fragility report with evidence. |
check | Flag changes that touch a do-not-break invariant (local + CI guard); also flags (advisory) symbols you add that already exist elsewhere. |
veto | Decision Guard — flag changes that reverse a rejected alternative. |
ci | Scaffold the CI Constraint Guard (GitHub Action). |
record-constraint | Record an invariant the codebase must not break. |
record-bug | Capture a Bug from a failing test (symptom + suspects). |
test | Run tests; capture failures as Bugs, promote recurrences to Constraints. |
supersede | Mark one decision as replaced by another (invalidate, don’t delete). |
stale | List decisions/constraints whose files changed after last verified (drift). |
review | Triage low-confidence drafts: list, accept, or reject. |
firmness | Get or set how firmly the agent hook enforces Hunch. |
embed | Generate local embeddings for semantic search (opt-in). |
compact | Prune low-value auto-captured records. |
migrate | Upgrade .hunch/ records to the current schema version. |
mcp | Start the MCP server over stdio. |
doctor | Diagnose 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/ ├─ 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`)
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).
hunch doctor| Symptom | Fix |
|---|---|
Assistant shows no hunch_* tools | Reload the assistant in the repo; if still missing, hunch doctor. |
Synthesis is deterministic | Log into a coding-assistant CLI (e.g. claude /login); drafts become high-confidence. |
| Graph looks stale | hunch index to rebuild the symbol graph from current source. |
| Schema mismatch after upgrade | hunch 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:
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.
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.