Code graph
Every session is indexed into a graph the agent can query structurally, instead of re-deriving the shape of your codebase by grepping it every task.
The server is codebase-memory-mcp — MIT, a single static binary with no runtime and no database. Measured on this repo: 0.46s to index, 3.4MB on disk, 821 nodes / 3813 edges.
Why this one
Section titled “Why this one”Six candidates were surveyed. What eliminated most of them was not quality:
| licence | external deps | verdict | |
|---|---|---|---|
| gitnexus | PolyForm-Noncommercial | — | most users, deepest integration, disqualified |
| codegraphcontext | MIT | Neo4j | a service to run is a decision to make |
| codegraph (npm), mcp-code-graph | — | — | a year stale |
| codebase-memory-mcp | MIT | none | chosen |
| @sdsrs/code-graph | MIT | node | close second |
A noncommercial licence cannot be a default. Every omh user writing code at work would be in violation of a dependency they never chose — a distribution doing that is doing something to its users rather than for them. gitnexus is a legitimate opt-in profile and never a base-set entry.
What was left decided itself: one static binary, no database, linux/arm64, 158
languages.
Three design consequences
Section titled “Three design consequences”- The graph lives in the base image, not a harness layer. It is harness-agnostic, and every session should get the same one.
- The cache volume is keyed by repo, not harness. That is what keeps the index warm across a switch from Claude Code to opencode. Tested.
- The checkout mounts read-only while indexing. An indexer that can write into your repo is a sandbox hole for no benefit.
Indexing runs inside the sandbox, because the cache is a container volume — an index built on the host lands where no session can read it.
Current, used, and visible
Section titled “Current, used, and visible”An MCP server on its own is inert: indexed once, never refreshed, never reached
for. Four hooks and a section of the rules are what make this a base-set entry
rather than an installed package — and all six are one feature, codegraph,
removed and disabled together.
| Hook | Fires | Cost | Buys |
|---|---|---|---|
graph-orient |
session-start |
2,300 B per context rebuild | modules, layers, boundaries, entry points |
graph-first |
before-tool, search² |
243 B per grep¹ | structural questions in one call |
graph-read |
before-tool, read |
0 unless it speaks | 1,511 bytes for one symbol instead of a whole module |
graph-refresh |
turn-end |
0.14s per turn | a graph describing the code as it is now |
Those are omh’s words, not a harness’s — see writing a hook.
What session-start is called inside Claude Code is the adapter’s business.
¹ For a ohmyharness-s01-length project name, which the nudge interpolates
twice — the figure moves two bytes per character of <repo>-<session>.
Computed from the shipped literals rather than typed in.
² Reaches no shipped harness today. omp and opencode have grep and glob as
separate tools, and Claude Code’s native builds (2.1.x) have neither — they
search through Bash — so each adapter leaves search unspelled and every
launch names graph-first as dropped rather than shipping a hook that never
fires.
Kept current. A session’s worktree is not the checkout it started from; it
holds whatever the agent has since written. Each session indexes its own, and
graph-refresh re-indexes when a turn ends. Measured at 0.45s cold, 0.14s
incremental — cheap enough that a stale graph is a choice rather than a
constraint.
Orientation beats interception. graph-orient is the only one that costs
nothing per tool call: the agent is given the module map instead of
discovering it by reading files. Everything else fires only when the agent is
already about to do something more expensive.
Nudges, never walls. Grep is right for a literal string, and a hook that
blocks correct work gets disabled. graph-read goes further and stays silent
unless a symbol lookup would actually be cheaper — a source file large enough to
be worth not reading whole. Read is the most frequent tool there is, and a
nudge on every call becomes noise the model learns to skip.
Hooks reference $OMH_GRAPH_PROJECT rather than baking in a session, so one
hook serves every session and the agent is told which graph is its own at the
moment it is choosing.
They are generated from the base manifest at launch, not files you can edit
— which is what lets omh ship a fix to one. Switching them off means switching
off the graph: omh set codegraph off for one repo, or
omh settings mcp rm codegraph to take the server out of your catalogue for
every repo. There is
deliberately no way to keep the graph and drop graph-refresh, because that is
the combination that manufactures confident wrong answers.
What the hook contract cost
Section titled “What the hook contract cost”Three things, none of them guessable:
- A hook reaches the model through
hookSpecificOutput.additionalContext, injected as a system reminder. Bare stdout on exit 0 is not that channel — the first nudge shipped that way and may never have been seen. Found by reading the spec, not by anything failing. SessionStartre-fires on resume and compact, so orientation is paid every time context is rebuilt, not once. That is why it sends four targeted aspects (2,138 B) rather thanoverview(6,173 B).--aspectsdoes not take a comma-separated list. That form returns empty, silently, which looks exactly like a hook that never fired. The flag repeats. Pinned by a test.
Which tools earn a hook
Section titled “Which tools earn a hook”The build ships 14 tools (check_index_coverage, from the marketing copy,
is not among them). Most are plumbing omh drives; a few are worth intercepting.
| Hooked | Because |
|---|---|
get_architecture |
orientation, and the only one free per call |
search_graph |
replaces iterative grepping |
get_code_snippet |
replaces reading a whole file for one symbol |
index_repository |
keeps the rest honest |
| Candidate | Trigger |
|---|---|
trace_path |
“how does A reach B” — the alternative is reading every file in the chain |
detect_changes |
reviewing a diff: structural impact rather than changed lines |
search_code is deliberately not hooked. It looks like a cheaper grep, but
--pattern 'fn unfilled' --mode compact returned total_results: 0 against a
symbol that demonstrably exists. Routing literal search through it would make
things worse, and the claim stays unverified until that is explained.
query_graph (Cypher) is a power tool the agent should reach for deliberately.
manage_adr, ingest_traces, get_graph_schema, index_status,
list_projects and delete_project are plumbing.
omh graph
Section titled “omh graph”$ omh graphomh: graph at http://127.0.0.1:56286 every session's graph for this repo, in one place stop with: omh graph --stopScope follows the data. Every session’s graph lives in one volume, so a per-session server showed every other session’s graph anyway — N identical websites on N ports. A repo-scoped service removes the duplication, survives sessions coming and going, and needs no session to exist at all.
It also mounts only the index — no worktree, no credentials, no profile. The per-session version ran inside a container holding a writable worktree and live credentials, which was exposure for no purpose.
Lifecycle became docker run / docker rm, idempotent by construction. That
deleted the pgrep guard, the detached exec and the pkill the per-session
version needed — each of which had been a bug before it worked. Choosing the
right scope removed them rather than fixing them.
What verifying the UI cost
Section titled “What verifying the UI cost”Five things were wrong, none findable from the documentation:
- The npm package cannot deliver the UI.
CBM_VARIANT=uiis documented, but the published 0.9.0 installer hardcodes the variant and never reads it — verified in a container, which still reported “built without the embedded UI”. omh fetches the release tarball directly, checksum-verified, with the arch fromTARGETARCH. - The UI dies with stdio. Backgrounded with stdin closed, it logs
ui.servingthenserver.shutdown. It runs behindsleep infinity |. - It binds container loopback, with no bind-address flag, so a published port
forwards to nothing:
HTTP 200inside, no response outside.socatbridges it. - A detached service cannot be spawned and abandoned.
A & exec Bleft the server running and the bridge dead. - The base image tag was a mutable
:latest, so rebuilds were skipped and base recipe changes had never shipped. Addingsocatsilently did nothing until the base tag got its own recipe digest — the same fix already applied to harness images, left half-done. Harness layers now pin an exact base.
What the agent can still see
Section titled “What the agent can still see”All graphs for one repo share a volume. That is what keeps the index warm across a Claude Code → opencode switch, and cross-repo isolation is intact.
But list_projects shows a session every other session’s graph for that repo,
and querying the wrong one answers confidently about code that is not in this
worktree.
Three mitigations, no guarantee: AGENTS.md names the project, the hook repeats
it at the moment of choosing, and omh sNN rm drops a session’s graph along with
the code it describes.
Hard isolation would need a volume per session — a full re-index each time, and no ability to compare branches. Worth doing only if this is observed to actually go wrong.
The honest caveat
Section titled “The honest caveat”None of the token numbers above are a benchmark. They are measurements of individual operations — what the graph costs and what a single lookup replaces — not evidence that it makes an agent better at tasks overall.
That distinction is deliberate and permanent. omh measures cost and argues benefit, rather than running an eval suite over a stochastic metric that would cost real money per decision and become the thing the base set is tuned to pass: measure the cost, argue the benefit.
So the honest claim for the graph is “it costs 2.3 KB per context rebuild and 0.14s per turn, and structural questions beat re-grepping” — the first half measured, the second half a judgment you are free to disagree with, and to remove in one command.