The map of where not to go.
It's a repo-local registry of dead ends — approaches that didn't work — committed to git alongside your code. Every clone inherits the map. Every agent (Claude Code, Cursor, Copilot, aider, whatever) that can shell out benefits from it.
No server. No network. No auth. Just a .jsonl file and a CLI.
uv tool install git+https://github.com/henstarr/deadweightInstalls dw on your PATH. Always pulls the latest from main.
cd your-repo
dw initThat's it. dw init does everything:
- Creates
.deadweight/with an empty registry - Appends a Dead Ends section to
AGENTS.mdandCLAUDE.md(creates them if missing) - Installs Claude Code
SessionStart+Stophooks in.claude/settings.json
Then commit the result so every clone has the same setup:
git add .deadweight/ AGENTS.md CLAUDE.md .claude/settings.json
git commit -m "chore: init deadweight"After dw init, Claude Code fires two hooks automatically — no manual steps required.
On session start (SessionStart hook):
[deadweight] 3 dead ends logged in this repo. Query before non-trivial approaches: `dw query --approach ...`. Log abandoned approaches: `dw log --approach ...`.
abc12345 owner/repo monkeypatching Query._execute
def67890 owner/repo direct SQLite writes to bypass ORM
...
Claude sees the count and relevant dead ends before writing a single line of code. If files are changed in your working tree, the hook surfaces dead ends matching those paths first; otherwise it shows the 5 most recent.
On session end (Stop hook):
[deadweight] 3 dead ends on record. Log any abandoned approaches with `dw log`.
A short reminder to log anything that failed before closing out.
Both hooks are non-blocking — they never prevent Claude from responding.
Before trying an approach:
dw query --approach "monkeypatch Query._execute"If a match comes back, skip it. The reason is in the record. Search covers both the approach description and the failure reason.
After giving up on an approach (3+ turns wasted):
dw log \
--approach "monkeypatching Query._execute" \
--reason "breaks transaction isolation in tests" \
--turns-wasted 14dw log writes to disk immediately — the entry is available to every agent in the same clone right away.
Share with your team:
dw sync # git add + commit the jsonl
git pushdw sync is only needed when you want to push the knowledge to a shared remote. dw log already persists locally — teammates on the same machine or in the same clone see it immediately after git pull triggers an automatic index rebuild.
If an approach is unblocked (library update, code change):
dw update <id> --resolvedMarks the dead end as resolved. It stops appearing in queries and at session start. The history is preserved in the JSONL for auditing.
| Command | What it does |
|---|---|
dw init |
Set up .deadweight/, inject docs, install hooks |
dw log --approach "..." --reason "..." --turns-wasted N |
Record a dead end |
dw query --approach "..." |
Search dead ends (FTS with BM25 ranking) |
dw update <id> --resolved |
Mark a dead end as resolved |
dw list |
Recent unresolved dead ends (default: 20) |
dw show <id> |
Full JSON for one entry |
dw insights |
Aggregate report: total turns wasted, hot paths, agent breakdown |
dw sync |
git add + commit the jsonl (for sharing; dw log already persists locally) |
dw rebuild |
Rebuild SQLite index from jsonl (runs automatically on git pull) |
dw check |
One-line status used by hooks |
.deadweight/
deadends.jsonl # source of truth — committed to git
deadends.db # SQLite index — gitignored, rebuilt automatically
config.yaml # repo id, sync branch
Writes append to the jsonl. Reads hit the SQLite FTS5 index, which rebuilds itself whenever the jsonl is newer (e.g. after git pull). Search uses BM25 ranking over both the approach and reason fields — a query for "module import failure" will match entries that describe it differently.
Committing the jsonl is the whole point — dead-end knowledge travels with the code.
The agent field tracks which tool logged a dead end. An approach abandoned by Cursor gets seen by Claude Code in the same repo. A dead end logged by one developer's aider session shows up at the next developer's session start. The JSONL is the shared memory layer.
dw log --approach "..." --agent cursordw query and dw list show all entries regardless of which agent logged them. You can filter with --agent if needed.
| Field | Required | Notes |
|---|---|---|
repo |
yes | Auto-detected from git remote |
approach |
yes | What was tried — the primary search field |
reason |
no | Why it failed (also searched by dw query) |
turns_wasted |
no | LLM turns spent before abandoning |
path |
no | File or directory the approach touched (enables path-relevant hook output) |
agent |
no | claude-code, cursor, copilot, aider, windsurf, other |
version |
no | Commit SHA or release tag |
task_id |
no | External task id (SWE-bench, issue #) |
Any agent that can shell out works the same way. The AGENTS.md and CLAUDE.md sections injected by dw init tell your agent to run dw query before trying and dw log after giving up. For non-Claude agents, the hooks won't auto-fire, but the instructions in those files serve the same purpose.
Pass --no-hooks to skip hook installation if you're using a different agent or setting up hooks manually:
dw init --no-hooksEvery AI agent starts from zero — rediscovering every landmine in the repo. deadweight is the infrastructure for the negative space. See PHILOSOPHY.md.
uv sync --extra dev
uv run pytestMIT
