Context Engineering Series · Part 4

Teaching Claude to
Think in Graphs

Part 1 gave Claude memory, but 200+ files later that memory had started to drown it, because every session loaded everything whether it mattered or not. What I actually wanted was for it to load the right things, so a local knowledge graph took over the job manual curation used to do and started routing context by the entities a task actually touches.

0
Graph Nodes
0
Edges
0
Entity Seeds
151ms
Query Time

The Memory System Hit a Wall

The 5-layer architecture from Part 1 worked... Claude remembered across sessions, loaded wiki pages on demand, and applied rules consistently. But after six months of daily use it ran into a scaling problem the original design was never built to handle, and I didn't see it coming until the context started feeling noisy.

By then I had 217 memory files, and the flat MEMORY.md index had swollen to 222 lines against a 200-line budget, so every new memory quietly pushed an older one off the bottom. The wiki was at 86 pages, and every one I added meant a manual routing-table edit in CLAUDE.md. If you forgot the edit, Claude simply never found the page. On top of that, session startup surfaced the 3 most recently modified files, but recent and relevant aren't the same thing, and you could be deep in a slide deck and still get handed yesterday's deployment fix.

What was really going on is that every layer had its own idea of relevance. Memory ranked by modification time, the wiki leaned on a hand-maintained routing table, and the rules loaded everything regardless, so I had three mechanisms stacked on top of each other that were never actually connected. The question underneath all of it was always "what should Claude load for this task?", and three independent systems kept answering it without comparing notes, which is why the answer they assembled between them came out incomplete just about every time.

Turning Files and Entities Into a Graph

A knowledge graph ties those three systems into one structure you can actually traverse, so instead of asking "show me recent files," the session asks "which entities matter to this work, and which files discuss them?"

The files themselves are nodes, about 130 memory files, 65 wiki pages, and 11 rules files. So are the entities they talk about (12 accounts, 13 products, 10 systems), and so are the 25 concepts and 9 projects, which puts the whole graph at 379 nodes.

Five Edge Types Connect Everything

Edge Meaning and example
REFERENCESA file mentions an entity. pitch-craft.md → REFERENCES → "slide generation"
APPLIES_TOA wiki page serves a work context. security-governance.md → APPLIES_TO → "Apex development"
RELATES_TOTwo files share 3+ entities (IDF-filtered). pitch-craft.md → RELATES_TO → slide-visual-design.md
DISCUSSED_INAn entity appears in a memory file. "Agentforce" → DISCUSSED_IN → agent-harness-project.md
PART_OFAn entity belongs to a project. "AIP" → PART_OF → "Meridian Platform"

How Context Assembly Works With the Graph

Before: Manual Routing
# Session starts
1. Load ALL 11 rules files
2. Check routing table in CLAUDE.md
3. Show 3 most recent files by mtime
# Result: generic context
# 30% irrelevant per session
After: Graph Traversal
# Session starts
1. Extract signals from git state
2. Match signals to entity nodes
3. Traverse APPLIES_TO (3x) + REFERENCES
4. Rank files by traversal score
# Result: entity-relevant context
# 151ms total query time

Five Scenarios Against Mtime

I ran graph-based context against the old mtime-based approach across 5 real scenarios and set the bar at 3 wins out of 5 before I'd trust it, and the graph won all five.

Each one lines up what the old system would have surfaced against what the graph surfaces for the same session start.

Scenario 1: Working on AIP Slide Generation

With mtime, session start handed Claude whatever 3 files I'd edited last, which on a slide day was usually deployment notes, an unrelated session log, or NNAOV metrics, and none of that helps you build a deck. The graph matched the slide signals to entities and came back with these six instead.

Mtime
  • Whatever 3 files were edited last
  • Usually deployment notes, an unrelated session log, or NNAOV metrics
Graph
  • pitch-craft.md, slide-visual-design.md
  • golden-narrative-framework.md, design-token-system.md
  • feedback-imagen-cheesy.md, feedback-slide-design-gaps.md

The account analysis, the Apex debug, and the harness email all went the way the slide day did, with mtime handing back whatever I'd touched last and the graph pulling files tied to the task.

Task Mtime surfaced Graph surfaced
Preparing a customer analysis for Acme Nothing about Acme at all, just the 3 files touched last sharktank-account-blitz.md (references Acme)
corporate-grounding-system.md (applies to analysis)
feedback-verify-sources-before-building.md
Debugging an Apex deployment issue Random files, none of them Apex-related, and no development rules salesforce-development-rules.md (APPLIES_TO Apex)
agentforce-design-patterns.md
feedback-apex-metadata-deploy-workaround.md
Writing a customer email about Agent Harness No harness context unless a harness file happened to be the last thing edited, and no communication rules agent-harness-architecture.md
hybrid-reasoning-vs-harness.md
headless-360-architecture.md
communication-system.md, anti-slop.md

Scenario 5 was deploying a Cloud Run update, and mtime surfaced no deployment or security context and none of the TVM compliance rules, while the graph pulled gcp-deployment-patterns.md, gcp-security-compliance.md, integration-topology.md, security-architecture.md, and my sa-key-rotation.md notes.

Four Phases, All Shipped

Across two sessions I built the whole thing... graph construction, session integration, auto-indexing, and live tool enrichment. It all runs on local SQLite without any cloud infrastructure or API calls behind it, just entity extraction and traversal, which is the part I like most about it.

1

Graph Bootstrap

First I entity-extract every memory, wiki, and rules file, then seed 68 entities across 6 types (accounts, products, projects, systems, concepts). From there the CLAUDE.md routing table gets parsed into APPLIES_TO edges, every file is scanned for entity mentions, and RELATES_TO relationships are computed between any two files that share 3+ non-stop-word entities.

# Build the graph
$ bun run build-context-graph.ts
# Output: graph.sqlite
# 379 nodes, 5,120 edges
# Rebuilt from scratch in ~2 seconds

Phase 2: Session-Init Integration

With the graph built, the SessionStart hook could query it instead of the hardcoded routing table. It pulls signals out of git state (changed files, recent commits, branch name) and the working directory, matches them to entity nodes, and traverses APPLIES_TO (weighted 3x) and REFERENCES edges to rank the wiki and memory files that matter. If graph.sqlite is missing, it falls back to the old routing.

# Signal gathering (automatic on every session start)
git diff → file paths → entity matches
git log → commit words → entity matches
cwd → directory names → entity matches
# Traversal
matched entities → APPLIES_TO (3x) + REFERENCES (1x)
→ ranked wiki pages + memory files
# Fallback: if graph.sqlite missing, use old routing

Phase 3: Auto-Indexing

To keep the graph current, a PostToolUse hook fires on every Write or Edit to a memory, wiki, or rules file. It upserts that file's node, deletes its old REFERENCES edges, re-scans it for entity mentions, and recomputes RELATES_TO with the IDF filter, all in 61ms per file, which means there's no full rebuild and the graph keeps up as files change, get created, or get deleted.

4

Live Tool Enrichment

The last phase took the graph out of session startup and into the middle of the work. A second PostToolUse hook fires on every MCP tool call to the Salesforce server, pulls entity signals out of the tool input (account names, search queries, SOQL strings), queries the graph for matches, and surfaces the related memory and wiki files right alongside the tool results.

# Triggered on every Salesforce MCP tool call
crm_assets("Acme")
→ extract signals: ["acme"]
→ match entities → traverse edges
→ inject: 3 memory files + 2 wiki pages
# Performance: 55ms per tool call

Two Bugs From the First Build

Both bugs showed up the same way, as a graph that connected far more than it should have. The first was a false positive, where the entity "Exa" (a search API) was substring-matching "example," "exactly," and "examination," so it turned up in 91 out of 308 files, close to a third of the whole corpus. Switching to a word-boundary regex for terms under 5 characters took it from 91 references down to 19.

The second was an edge explosion. A handful of high-frequency entities (AIP in 44% of files, Agentforce in 42%, Slack in 35%) made nearly every file relate to every other one, and with the threshold at 2+ shared entities I ended up with 16,274 RELATES_TO edges, which made the graph fully connected and useless for ranking. I added an IDF stop-word filter (>25%) and raised the threshold to 3, and that brought it down to 2,877 edges, an 82% reduction.

Under the Hood

The entire system is local... SQLite running on your laptop, driven by a TypeScript build script and a handful of Python hooks, with no cloud database, no API calls, and no network anywhere in the loop.

Stack

Database SQLite (bun:sqlite)
Build script TypeScript (Bun)
Hooks Python 3 (stdlib only)
Full rebuild ~2 seconds
Session query 151ms
Incremental update 61ms per file
Dependencies Zero (stdlib only)
External services None

Schema (complete)

CREATE TABLE
nodes (id, node_type, name, canonical_name, file_path, properties, created_at, updated_at)
-- UNIQUE(node_type, canonical_name)
CREATE TABLE
edges (id, source_id, target_id, edge_type, confidence, properties, created_at)
-- UNIQUE(source_id, target_id, edge_type)
-- FK with ON DELETE CASCADE
Two tables and five indexes make up the whole schema.

What Was Live and What Got Parked

By the end of the build the graph ran both session startup and live tool enrichment, and I had two extensions left that were waiting on the platform.

LIVE

Graph-Enriched MCP Results

When a CRM query returns data about an account, the graph injects the related context automatically. Ask about Acme and Claude sees the prior analysis notes, the relevant feedback, and the wiki pages sitting right alongside the raw data, because a PostToolUse hook extracts entity signals from the tool input, queries the graph, and surfaces the top 5 related files in 55ms.

The first extension I parked was a re-query after the first message. Session-init runs before the user types, so it can't know intent, and a second graph query once you've said what you're working on would load topic-specific context mid-conversation. When I built this, Claude Code didn't have a PostUserMessage hook type, so there was nothing for that second query to hang off of.

The other one is task-specific rule loading. All 11 rules files loaded every session, and the graph could have worked out which rules actually apply to the task at hand, but Claude Code loaded every rule before any hook ran, so I parked it until the platform could load rules conditionally.

The Context Engineering Series

The graph gave Claude a map of how things connect. Part 5 puts that map to work, packaging the whole approach into skills Claude can reuse on its own.

Each part builds on the one before it, and you can start anywhere, though Part 1 is where the foundation gets laid.