DEV Community

Saravanan Jaichandaran
Saravanan Jaichandaran

Posted on

Your dogfooding output IS a bug report, a story about three empty tables

I ship world-model-mcp — a temporal knowledge graph MCP server that gives AI coding agents (Claude Code, Cursor, Copilot Chat, etc.) durable memory across sessions. It captures facts about your codebase, learned constraints from your corrections, and lifecycle events from every coding session.

A month after v0.11 shipped, I ran the graph on the world-model-mcp repo itself — dogfooded it and snapshotted what it captured. The numbers looked like this:

608 facts. 600 entities. 3 constraints. That should have been a headline: my own memory layer captured a rich picture of its own codebase.

Except three tables were empty.

*Events, decisions, and sessions, the tables that should have accumulated across a month of active development were all zero.
*

This post is about what I did with that anomaly, what it turned out to hide, and the general lesson I keep coming back to about dogfooding.

The first hypothesis was wrong
If you wouldd asked me on that morning what the three empty tables meant, I would have said: "the hooks aren't firing." Claude Code's session lifecycle fires SessionStart, PreToolUse, PostToolUse, and PostCompact hooks that my graph subscribes to. If those hooks weren't running, no events, no decisions, no sessions — the exact shape of what I was seeing.

The most likely reason a Claude Code hook doesn't fire is that the project doesn't have an ".mcp.json" at the repo root pointing at the MCP server. So I added one. Fifteen-line JSON file.

Two hours later, the tables were still empty.

Something else was going on. And this is the part where dogfooding stopped being a nice-to-have and became a debugging tool.

The transcripts don't lie
Claude Code writes every session to ~/.claude/projects//*.jsonl. Each hook invocation, each tool call, each error all captured. I opened the transcripts for this project and started grepping for SessionStart. Every single invocation, going back to the first one on 2026-06-15, showed the same silent failure:

hookName: SessionStart:startup
exitCode: 1
stderr: Error: Cannot find module '/xxx/xxx/claude'
Node.js v23.11.0
Wait. Cannot find module '/xxx/xxx/claude'? That's not a full path. That's a fragment of my project path.

The project lives at:

/xxx/xxx/claude context graph/world-model-mcp/

Two spaces. claude context graph. If you split that on whitespace, you get claude, context, graph/world-model-mcp/… — three separate strings.

That's exactly what happened.

The bug I had been shipping for months

Every hook command in my generated .claude/settings.json used the environment variable $CLAUDE_PROJECT_DIR without quotes:

{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/world-model-session.js start"
}
]
}
]
}
}

When Claude Code expanded $CLAUDE_PROJECT_DIR at shell time and the value contained a space, the shell split the expansion on whitespace. Node received:

  • arg 1: /xxx/xxx/claude — treated as the module argument, doesn't exist
  • arg 2: context
  • arg 3: graph/world-model-mcp/.claude/hooks/world-model-session.js
  • arg 4: start

Hence the error message: Node reports the first argument as the missing module, so the stderr said Cannot find module '/xxx/xxx/claude__' — a name that no user searching their own logs for troubleshooting would ever recognize.

*Zero user-facing error. Zero visible clue. The database just stayed empty.
*

And it wasn't only my problem. Anyone whose project path contained a space would hit this. That's:

  • macOS defaults: ~/Documents/…, ~/Desktop/…
  • Corporate paths: ~/Work Stuff/Client X/…
  • Or intentional folder names like my own claude context graph/

Every one of those users, silently, since v0.7.3 shipped hooks in early 2026. Months of silent failure.

The fix took two lines
Wrap $CLAUDE_PROJECT_DIR in double quotes in every generated command string:

  • "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/world-model-session.js start"
  • "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/world-model-session.js\" start"

Add a regression test:
def test_setup_command_generates_quoted_project_dir(tmp_path):
"""v0.11.0 regression: unquoted $CLAUDE_PROJECT_DIR broke on paths with
spaces. The bundled settings.json must double-quote every expansion."""
from world_model_server.setup import setup_command
setup_command(project_dir=tmp_path)
settings = json.loads((tmp_path / ".claude" / "settings.json").read_text())
for event_hooks in settings["hooks"].values():
for entry in event_hooks:
for hook in entry["hooks"]:
cmd = hook.get("command", "")
if "$CLAUDE_PROJECT_DIR" in cmd:
assert '"$CLAUDE_PROJECT_DIR' in cmd, (
f"Unquoted $CLAUDE_PROJECT_DIR in hook command: {cmd}"
)

Ship it in v0.11.0 with an entry in RELEASE_NOTES.

That's the mechanical part. What actually matters is the pattern I want to argue for.

The load-bearing lesson
Here is what I keep coming back to: when a snapshot shows a shape that doesn't match your mental model, that anomaly is the signal, even if nothing else surfaces.

Three tables empty in a database with 608 facts and 600 entities isn't a database problem in the absolute-count sense. If you were looking at the total row count you'd see 1,211 rows and think "wow, that's a lot of memory." The failure hides in the ratio — dense data in some places, zero in others, with no external reason for the divergence.

That's the shape dogfooding is uniquely suited to catch:

Zero-elsewhere alarms: your test suite doesn't complain (green). Your CI doesn't complain (green). Your users don't complain (they didn't know the tables were supposed to have data). The absence-of-signal problem is exactly the one no signal-based observability catches.
Anomaly detection via personal mental model: I knew the events table should have hundreds of rows from a month of coding. Nobody else knew that. You need someone using the tool with real intent to notice — and that someone has to be paying enough attention to look at what's there instead of just consuming what the tool serves.
Compound signal from adjacent-tables comparison: 608 facts against zero events is more suspicious than either number alone. The contrast is the tell.

I think of this as the "empty room" problem in tool-building. A tool that gives you back nothing might be broken. A tool that gives you back partial results is definitely broken. And the way you catch partial results is by knowing what the shape should look like which only happens if you use the tool on your own real work.

Follow-up: v0.12.1 shipped a diagnostic for this whole class

The specific bug is now fixed — the shell-quoting issue can't recur in newly generated settings.json files. But installs that predate the fix (or that drift over time for other reasons) can still silently fail. So v0.12.1 shipped a new command:

world-model doctor --project-dir .

Eight diagnostic checks including:

  • .claude/settings.json presence
  • Shell-quoting of $CLAUDE_PROJECT_DIR in every hook command (specifically catches the pre-v0.11.0 bug pattern)
  • Hook script presence
  • .mcp.json registration
  • DB directory + stale queue
  • Claude Code hook error history, filtered by settings.json mtime (so historical failures pre-date-fix WARN rather than FAIL)

--json for machine-readable output. --fix for safe auto-rewrites. On the maintainer's own repo now: 7 pass, 1 warn (pre-fix history), 0 fail.

Would doctor have caught the original bug in April? Absolutely — the shell-quoting check is the bug pattern. That's the point.

The class of latent bug that dogfooding catches once is the class of latent bug you should be able to diagnose forever after.

What I want you to take away

Two things.

One: if you're building any kind of stateful tool memory layers, caches, event pipelines, telemetry, start looking at what your tool captures about itself running on your own work. Not synthetic fixtures. Not test data. What did it record when you ran it on your real repo? Does the shape look right? If a table you expected to have data is empty, that's not a database problem, it's a bug report waiting to be transcribed.

Two: the specific tool matters less than the discipline. I use world-model-mcp because I built it. If you build tools and don't dogfood, that's the loss. If you build tools and do dogfood but never look at the shape of what got captured, that's the loss too. The looking is the whole practice.

Links

  • Full case study with reproducibility contract: case-studies/v011-dogfooding/CASE_STUDY.md
  • world-model doctor implementation: world_model_server/doctor.py
  • Repo: SaravananJaichandar/world-model-mcp

If you want to try it: pip install world-model-mcp && world-model setup

Top comments (0)