DEV Community

Debugging basemind with Claude Code

basemind is my code-intelligence tool: a pure-Rust code map and scanner, tree-sitter across 300+ languages, a content-addressed blob store, a Fjall-backed inverted index. I harden it by pointing it at a repo big enough to hurt, a 20M-line, 200k-commit monorepo, and watching what it does.

I did this debugging session with Claude Code. It started as "which version is installed?" and ended with a 7x speedup, two upstream PRs, and a feature turned off. Here is how I worked, and the methods that made it effective.

The symptom is not the bug

The report: basemind's cache grew from 1.8 GB to 4 GB while CPU pinned for a long time. I did not open the scanner code. I looked at the live system. ps and lsof found a process at ~70% CPU holding the index open, and it was a serve process, not a scan. Different bug entirely.

That process was an old release. So I had Claude dig through git instead of the debugger: git log on the watcher, then git merge-base --is-ancestor and commit dates. A commit titled "fix: stop watcher CPU runaway" was dated four days after the running binary shipped. The bug (a watcher re-scanning its own writes forever) was already fixed. It was a stale process left over from another editor session.

Two rules, both applied before reading a line of source:

  • Verify the running artifact, not a proxy. Homebrew, the PATH, and the editor plugin each reported a different version. Find the one that actually runs.
  • "Live bug or old build?" is the first question. Dating the fix against the running version can end an investigation before it starts.

Measurement tells a scan from a leak

I killed the ghost, and a fresh scan appeared at 488% CPU, nearly five cores. That is rayon pegging every core, and it is indistinguishable from a runaway loop on a single reading.

So I sampled three signals over time: CPU%, cache size, blob count. Disk climbed then plateaued. Blob writes burst then stopped. CPU fell to one core. That is a one-time rebuild converging, not a leak. A leak keeps allocating, a loop keeps writing, a scan converges. The derivative is the diagnosis, not the number.

To watch without babysitting, I had Claude write background monitors: shell loops sampling every 30s, printing only on a state transition. One fired on "SCAN COMPLETE + IDLE," another on "0% CPU and no disk growth for 4 minutes." The alert encodes the hypothesis: a hang is low CPU and no progress, a healthy scan is high CPU or growing disk.

Isolate the confound

A multi-worktree test then hung: scans wedged at 0% CPU, and lsof on a stuck process itself hung for two minutes. A hanging lsof means a stuck socket. The last log line was Registering VLM OCR backend. The OCR tier made a network call with no timeout, a firewall dropped it, and the connection blocked forever.

I did not debug the hang in place. That tier is opt-in behind a cargo feature, so I built basemind with default features, a binary that cannot make the call, and studied the scanner in isolation. When two failures tangle, change one variable.

Read for the mechanism, then measure the fix

On a clean binary I took a baseline. A fresh worktree scan took 181s, re-parsed all 67,700 files though their blobs already existed, and rebuilt the git-history index per worktree at ~160s. Now I read code, to confirm the mechanism, not guess a patch.

  • The "unchanged file" fast paths keyed off the per-worktree index, empty on a fresh worktree. So every file fell through to a full re-parse, despite its blob already sitting in the shared store.
  • The git-history index opened at the worktree's own cache dir, though it derives entirely from the shared .git.

Both fixes followed patterns already in the code, and each got a regression test written to fail first. Re-measured: 181s to 25s, ~7x, all 67,700 files reused, peak RSS about halved. The re-measurement is the point. It turns "should be faster" into "is 7x faster."

Follow the failure across the boundary

The network hang still deserved a fix. I traced it: the OCR and embedding models download through hf_hub, whose builder in 0.4 and 0.5 constructs a ureq agent with no timeout. A firewalled download blocks forever. The root cause was in the dependency.

I forked it and had Claude map every download call site across two crates, then add a watchdog: run the blocking fetch on a detached thread, error on a tunable deadline, degrade by skipping the model instead of hanging. The mechanical multi-file edit went to a subagent with a tight spec. Then I read the diff and reran the build and tests myself. The subagent even corrected my assumption about a type not being Clone. That shipped as an upstream PR plus an honest bug report. Delegate the mechanical work, verify with diffs and reruns, never a summary.

Sometimes the fix is deletion

Monitoring caught the embedding pass stuck on a general-purpose model, an empty model dir with a stale lock, spinning a core. The sharper question was not why it was slow, but whether code embeddings should exist at all.

I traced what gets embedded (symbol, signature, doc, body) against what embeds it (a general-English model), and basemind already builds a BM25 index over the same text. A prose model embeds code weakly, the one real win (natural language to symbol) is already covered by the keyword lane, and the cost is a 100 MB pull plus gigabytes of vectors. Verdict: code embeddings off by default, kept for docs and images. A feature that is off cannot hang, leak, or pin a core.

The methodology

  1. Observe the live system (ps, lsof, du) before reading source.
  2. Separate stale-artifact bugs from live bugs by dating the fix against the running version.
  3. Measure dynamics, not snapshots. The derivative separates converging from leaking.
  4. Encode the hypothesis into a monitor so the machine watches for you.
  5. Isolate confounds by changing one variable.
  6. Read code for the mechanism, then write the smallest fix.
  7. Prove it with a test that fails without the fix, and re-measure the metric you started from.
  8. Follow failures into dependencies, and report them honestly.
  9. Ask whether the costly feature should exist, not just why it broke.
  10. Delegate the mechanical work to an agent, but verify every diff yourself.

What shipped

  • Worktree cache reuse: cold scan 181s to 25s (~7x), 67,700 files reused, peak RSS about halved.
  • Code embeddings off by default, per-file embed_exclude, safe preset swapping.
  • An upstream timeout guardrail in the dependency, plus a bug report.

The method I kept coming back to: verify the artifact, watch the live system, measure the change over time, isolate the confound, read for the mechanism, and prove the fix by the number you started from. basemind lives at github.com/Goldziher/basemind.

Top comments (0)