Persistent design memory for Claude-assisted codebases. Before you edit a file, EchoDev surfaces why the module was built that way. After you commit, it records the new rationale into a structured decision graph.
npm install -g @hey-echodev/cli
echodev --helpOr one-off without installing:
npx --package=@hey-echodev/cli echodev <command>cd /path/to/your-project
echodev init # creates .echodev/ and .claude/skills/
echodev list # (empty — no decisions yet)
# Seed a decision:
echo '[{
"slug": "auth-jwt-cookie",
"problem": "Where do session tokens live on the client?",
"decision": "HttpOnly cookie, never localStorage.",
"affected_files": ["src/auth/**"],
"future_reminders": {
"who_might_repeat": "engineer adding SSO",
"revisit_when": "a cross-origin client needs tokens in JS"
}
}]' | echodev add --stdin
echodev recall src/auth/login.ts # returns the seeded decision
echodev graph --format mermaid # renders the decision graphechodev init [--no-claude]
echodev recall <paths...> [--modules a,b] [--keywords x,y]
[--top K] [--min-score N] [--quiet] [--format json|text]
echodev extract <ref> [--kind commit|diff|pr|manual] [--llm auto|api|skill|null] [--force]
echodev add --stdin [--ref <label>]
echodev check <diff-file>
echodev list [--status active|superseded|expired] [--format json|text]
echodev graph [--format mermaid|json]
echodev init drops two skills into .claude/skills/ and prints a hook recipe to stdout for you to paste into .claude/settings.json:
echodev-recall—echodev recall <file>before any non-trivial edit.echodev-record— captures decisions from a completed change.
EchoDev never auto-edits .claude/settings.json — Claude Code best practices recommend you own that file. Re-running echodev init is safe; it detects an existing echodev recall entry and skips the recipe.
For deeper guidance, see
docs/best_practice.md— a user-facing best-practices guide where every rule cites both a Claude Code anchor and an EchoDev design anchor.
Copy these entries into your .claude/settings.json (under the top-level "hooks" key):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "test -d .echodev && command -v echodev >/dev/null 2>&1 && echodev recall --from-stdin --quiet --format text || true"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "test -d .echodev && command -v echodev >/dev/null 2>&1 && git rev-parse HEAD >/dev/null 2>&1 && echodev extract HEAD --kind commit --llm auto || true"
}
]
}
]
}
}PreToolUseonEdit|MultiEdit→ silent recall (gated on.echodev/andcommand -v echodev)Stop→ idempotentextract HEAD(same gates +git rev-parse HEAD)
PreToolUse consumes Claude Code's documented hook input — a JSON object on stdin with a tool_input.file_path field. recall --from-stdin parses it; the contract surface lives inside this CLI so the recipe stays POSIX-pure (no jq dependency) and so a future Claude Code schema change needs one fix here, not in every consumer's settings.json. The command -v echodev guard keeps the hook silent on machines where the CLI isn't installed.
Tested against Claude Code as of 2026-04. If the hook stops returning recall results after a Claude Code update, the contract may have shifted — open an issue or check recall --from-stdin against the latest hooks reference.
After merging, run /hooks inside Claude Code to verify (or restart Claude Code).
Claude wastes context re-deriving what your project already decided. The naive fix ("dump the wiki") makes it worse.
Thesis: inject the fewest, most load-bearing prior decisions, at the moment the editor needs them, and stay silent otherwise.
Three commitments:
- Decision unit, not document. Each node has
problem,decision,alternatives,constraints,failures,expiry_conditions,future_reminders{who_might_repeat, revisit_when}. - Context-aware retrieval. File glob (1.0) → module (0.6) → keyword (0.3) → one graph hop. Embeddings are an optional re-ranker, never the base.
- Prefer silence over noise. Empty queries, weak hits, and already-extracted commits all produce zero output.
| Guard | Default | Why |
|---|---|---|
recall --min-score |
0.5 |
Drops lone-keyword hits (0.3). File/module/multi-keyword survive. |
recall --top |
5 |
Hard cap. Never injects more than ~5 decisions (~20 lines). |
| Empty query → empty output | always | No paths/modules/keywords → no dump. |
recall --quiet |
used by hooks | Zero hits print nothing — no breadcrumb on every edit. |
| Graph expansion | 1 hop, re-gated by min-score |
Neighbours score × 0.5. |
extract idempotency |
on | .last-extracted marker — safe to bind to Stop. |
| Hook repo gate | test -d .echodev |
No cross-project leakage. |
| Hook matcher | Edit|MultiEdit |
Write excluded — new files have no prior decisions. |
Cost scales with decision count, not project size.
Each decision is one JSON file under .echodev/decisions/<id>.json:
Full schema: schema/decision.schema.json — language-agnostic, any tool can read/write.
Every decision file carries schema_version. The current version is "1.0".
- Writers stamp it.
echodev addandechodev extractsetschema_version: "1.0"on every decision they produce. - Readers fail-closed. Any decision missing
schema_version, or carrying a value the running CLI doesn't know, is rejected with an error pointing toechodev migrate. This prevents silent corruption when the schema evolves. echodev migratewalks a forward chain.packages/cli/src/commands/migrate.tsdeclares aMIGRATIONSregistry — currently one step (undefined → "1.0", backfilling pre-versioned legacy files). The walker applies whatever steps lead from the file's current version to the target.- Adding v2 is one append. When a future schema change ships, add a
{from: "1.0", to: "1.1", apply: ...}entry to the chain.echodev migratepicks it up; existing files migrate forward in a single pass; no other code changes. - No downgrade. A file carrying a version newer than the running CLI knows about has no chain step → migrate refuses with a clear error rather than corrupting it. Upgrade the CLI instead.
- Commit:
.echodev/decisions/,.echodev/.gitignore - Ignore (auto-managed, per-clone):
.echodev/index/,.echodev/bridge/— these are rebuilt fromdecisions/on demand and include a local idempotency marker; they should never be shared.
--llm auto|api|skill|null:
- auto (default) — Claude API if
ANTHROPIC_API_KEYis set, else Skill Bridge - api — force Claude API
- skill — force Skill Bridge (no API key; skill fills the prompt from Claude Code)
- null — never call an LLM (CI smoke tests)
Clone the repo, run the bundled sample:
git clone https://github.com/jieyao-MilestoneHub/EchoDev
cd EchoDev
npm install
npm run build
node packages/cli/dist/index.js --repo examples/sample-repo listThe core logic is free functions behind typed ports (DecisionReader,
DecisionWriter, Extractor, Retriever, LLMClient). Adapters live in
packages/{storage-fs,extractors,retriever,llm}. The composition root is
packages/cli/src/composition.ts — add a new LLM client or storage backend
by writing a new adapter and wiring it there.
MIT © jieyao-MilestoneHub
{ "schema_version": "1.0", "id": "d-YYYY-MM-DD-<slug>", "status": "active | superseded | expired", "problem": "...", "decision": "...", "alternatives": ["..."], "constraints": ["..."], "failures": ["..."], "expiry_conditions":["falsifiable signals this no longer applies"], "affected_files": ["globs"], "affected_modules": ["logical"], "relations": { "inherits": [...], "conflicts_with": [...], "fills_gap_of": [...], "shared_premise": [...], "superseded_by": null }, "future_reminders": { "who_might_repeat": "...", "revisit_when": "..." }, "source": { "type": "commit|pr|issue|manual", "ref": "..." } }