๐ŸŒณ SQLite property graph ยท FTS5 ยท Semantic chunks ยท Hook-intercepted tools

Every tool call builds a graph.
Claude gets references, not raw data.

ctx-tree intercepts all Claude Code native tools and all MCP tool calls โ€” reads, searches, web fetches, commands, agent invocations, skills โ€” and routes them through a persistent SQLite property graph. Context shrinks 95โ€“99%. Knowledge accumulates across sessions.

๐Ÿ”ฅ

The Problem: Context Rot

Every tool call dumps raw file contents, grep output, and web pages straight into the context window. A session that reads 8 files, runs a few web fetches, and calls some MCP tools burns 60โ€“80k tokens on raw data โ€” leaving almost nothing for reasoning. The session stalls before the work is done.

๐ŸŒณ

The Solution: Hook Everything

ctx-tree intercepts every tool call via Claude Code hooks โ€” native and MCP. Content lands in a persistent SQLite property graph, chunked by symbol and indexed. Claude gets back a compact nodeId reference, not raw bytes. The same file read next week costs zero tokens โ€” it's already in the graph.

๐Ÿš€

The Payoff: 10ร— Sessions

Sessions that previously stalled at 40k tokens now run to completion. Knowledge accumulates across sessions โ€” prior reads surface instantly via ctx_tree_compose. You can explore an entire codebase, debug a production incident, and ship a feature all in one session. Context rot disappears.

๐Ÿ“„ Read the spec โ†’

โŒ Without ctx-tree

Read file
~4,200 tok
Grep (8 files)
~8,400 tok
WebFetch
~11,000 tok
MCP tool
~3,600 tok

โœ… With ctx-tree

Read file
~60 tok
Grep (8 files)
~160 tok
WebFetch
~110 tok
MCP tool
~70 tok
๐Ÿ—„

The Property Graph

SQLite ยท FTS5 ยท Per-project ยท Content-hash deduplication ยท Click any node to explore

๐Ÿ”—
Hook fires
PreToolUse
โ†’
๐Ÿšซ
Deny native
tool blocked
โ†’
๐ŸŒณ
ctx_tree_*
MCP tool runs
โ†’
๐Ÿ’พ
Node stored
SQLite graph
โ†’
๐Ÿ“Ž
Reference
returned to Claude
โ†’
๐ŸŽฏ
compose()
budget-bounded recall

Node types โ€” click any to inspect

Edge types

Readโ†’ctx_tree_read
PreToolUsedeny

File content is semantically chunked (functions, classes, modules) into file_chunk nodes linked by next edges. Each chunk carries source_uri, content, size, and embedding. Claude gets compact references instead of raw source. Click any node to explore its properties.

Without ctx-tree~4,200 tokens
Claude calls
Read({ file_path: "/src/auth/login.ts" })
Raw output dumped to context
// 587 lines of TypeScript โ€” auth/login.ts import { db } from '../db' import jwt from 'jsonwebtoken' export async function login(email, pw) { const user = await db.users.findOne({ email }) // ... 580 more lines follow ...
With ctx-tree~62 tokens
Hook denies Read โ†’ calls
ctx_tree_read({ path: "/src/auth/login.ts" })
Returned to Claude
ctx_tree_read stored 4 chunks: fc-a1b2 login.ts#imports 152B fc-c3d4 login.ts#login 487B fc-e5f6 login.ts#validateToken 312B fc-g7h8 login.ts#logout 198B
Saved 4,138 tokens โ€” 98.5% reduction
๐Ÿ“„ Read ยท session โ†’ turn โ†’ tool_call โ†’ file โ†’ semantic chunks
session turn tool_call file file_chunk contains reads next
Grepโ†’ctx_tree_grep
PreToolUsedeny

FTS5 full-text search stores matched files as file_chunk nodes linked via result edges. Click any file node to expand its semantic chunk stream inline. Re-runs return cached node IDs instantly.

Without ctx-tree~8,400 tokens
Claude calls
Grep({ pattern: "useAuthContext", glob: "**/*.tsx" })
// Header.tsx (lines 12-28): const { user } = useAuthContext() // Sidebar.tsx ยท Profile.tsx ยท Dashboard.tsx ...
With ctx-tree~160 tokens
Hook denies Grep โ†’ calls
ctx_tree_grep({ pattern: "useAuthContext" })
Found 4 matches โ€” stored as nodes: fc-h1i2 Header.tsx#useAuthContext fc-j3k4 Sidebar.tsx#useAuthContext fc-l5m6 Profile.tsx#useAuthContext fc-n7o8 Dashboard.tsx#useAuthContext
Saved 8,240 tokens โ€” 98.1% reduction
๐Ÿ” Grep ยท click file nodes to expand chunks
session turn tool_call file_chunk result next (chunks)
WebFetchโ†’ctx_tree_browse
PreToolUsedeny

Fetched pages are stored as web_content nodes with an accessibility tree, ref numbers, and content chunks. Click the web_content node to see the full accessibility tree โ€” each element has a ref you can use with Bash tools.

๐ŸŒ WebFetch ยท click web_content to inspect accessibility tree
session turn tool_call web_content references
Bash / Monitorโ†’ctx_tree_monitor
PreToolUsePostToolUsecapture

Long-running commands stream output as output_chunk nodes via next edges โ€” like a Python generator. Read-only mode captures stdout. Read-write mode also allows sending stdin to the process.

๐Ÿ’ป Monitor read-only ยท npm test --watch streaming
tool_call output_chunk derived_from next
๐Ÿ’ป Monitor read-write ยท python repl ยท send + receive
tool_call output_chunk input_chunk next sends
WebSearchโ†’ctx_tree_search
PreToolUsedeny

Search results are stored as web_result stub nodes with URL, status, title, etag, and last-updated. Failed fetches are marked red. Any result can be expanded via ctx_tree_browse.

๐Ÿ“ก WebSearch ยท result nodes with status โ€” red = failed
web_result (200) web_result (error) result
Agentโ†’sub-session graph
enrichedcapture

Agent tool calls create a nested subgraph โ€” its own session, turns, tool calls, and results. When complete, the subgraph collapses to a summary node returned to the calling agent. The full subgraph remains inspectable.

๐Ÿค– Agent ยท subgraph collapses to summary โ€” click to expand/collapse
session turn tool_call summary summarizes
Skillโ†’skill graph
PreToolUsePostToolUse

Invoking a Skill creates a skill node backed by a SKILL.md file. The file is chunked into step/diagram chunks, linked to referenced assets, and connected to its plugin. Cached on second load via content hash.

โœฆ Skill ยท SKILL.md โ†’ chunks โ†’ assets โ†’ plugin
skill skill_chunk plugin file (asset) contains references
MCP Toolsโ†’mcp_tool nodes
MCPPreToolUsePostToolUse

ctx-tree intercepts all MCP tool calls, not just native Claude Code tools. Every MCP server invocation creates a mcp_tool node linked to the mcp_server and tool call result, storing arguments, output, and timing metadata.

โš™ MCP ยท session โ†’ turn โ†’ tool_call โ†’ mcp_server โ†’ result
mcp_server mcp_tool output_chunk derived_from
๐Ÿ’ฌ

Prompt Anatomy

Every turn is a linear graph: prompt โ†’ thinking โ†’ response โ†’ tool_calls โ†’ questions โ†’ choices

Turns descend from sessions. Each turn contains a sequential chain: user prompt, LLM thinking blocks, response text, tool calls, and AskUserQuestion nodes with choice nodes. Selected choices continue the conversation (green).

๐Ÿ’ฌ Prompt anatomy ยท session โ†’ turn โ†’ prompt โ†’ thinking โ†’ response โ†’ question โ†’ choices
prompt thinking response question choice (selected) choice
๐ŸŒณ

Full Conversation Tree

A non-trivial session โ€” drag nodes, scroll to zoom, click to inspect

๐ŸŒณ Full convo ยท force-directed ยท drag ยท zoom ยท click nodes
โง–

Hook Pipeline

PreToolUse ยท PostToolUse ยท SessionStart ยท UserPromptSubmit ยท Stop

โง– Hooks ยท tool invocation โ†’ pre-hook โ†’ allow/deny โ†’ execute โ†’ post-hook โ†’ capture
hook tool_call triggers
โง–

Hook Reference

Every hook, when it fires, and exactly what it does

SessionStart session-start.mjs startup ยท resume
When it fires
On session startup or resume events. Skips compaction and clear noise so the injection doesn't repeat mid-session.
What it does
Injects the full ctx-tree tool usage guide into context โ€” always-on redirects, graph navigation commands (ctx_tree_neighbors, ctx_tree_search, ctx_tree_compose, ctx_tree_recent), and the readโ†’nodeIdโ†’compose workflow pattern.
โœฆ additionalContext injected
UserPromptSubmit userpromptsubmit-capture.mjs all prompts
When it fires
Every time the user submits a non-empty prompt. Requires ctx-tree store to be initialized for the project.
What it does
Stores the prompt as a prompt node in the graph with a content-hash URI. Injects the resulting nodeId into context so subagents can reference the prompt without re-reading it. Prior prompts surface in future sessions via ctx_tree_search.
โœฆ nodeId injected
PreToolUse pretooluse-redirect.mjs Read ยท Grep ยท Bash ยท WebFetch ยท Monitor
When it fires
On Read of any code or config file extension (.ts, .py, .json, .md, .yamlโ€ฆ). On Grep. On Bash commands containing grep, rg, ag, cat, bat. On WebFetch. On Monitor. On PowerShell equivalents (Select-String, Get-Content, Invoke-WebRequest).
What it does
Hard-denies the native tool call and emits the exact ctx-tree replacement (ctx_tree_read, ctx_tree_grep, ctx_tree_browse, ctx_tree_monitor) as additionalContext. Claude sees the denial as a tool error and immediately retries with the correct ctx-tree tool โ€” no round-trip wasted.
โœ• permissionDecision: denyโœฆ replacement injected
PreToolUse pretooluse-agent-enrich.mjs Agent tool
When it fires
Every time Claude invokes the Agent tool to spawn a subagent. Fires before the subagent starts, giving it access to prior session context.
What it does
Lazily creates a session node. Injects the ctx-tree tool map into the subagent prompt. If the store is populated, runs an FTS keyword search on the task text and prepends the top hits so the subagent starts with relevant context. Writes a state file timestamped for the PostToolUse capture hook to collect after the subagent finishes.
โœฆ additionalContext in subagent prompt
PreToolUse pretooluse-skill-recall.mjs Skill tool
When it fires
Every time Claude invokes the Skill tool. First-time loads pass through normally. On repeat invocations (same or different session), the hook short-circuits disk I/O.
What it does
Looks up skill://<name> in the graph store. If found, denies the Skill tool and injects a compact reference pointing to the cached node โ€” Claude calls ctx_tree_compose([nodeId], budget) instead of reloading from disk. Cache miss: passthrough, skill loads normally.
โœ• deny (cache hit)โ†’ passthrough (miss)
PreToolUse pretooluse-proxy.mjs Read ยท Grep ยท WebFetch (proxy mode)
When it fires
Only active when ~/.ctx-tree/proxy-mode flag file exists. Provides a lighter alternative to the hard-redirect hook โ€” advises rather than denies.
What it does
Rewrites the native tool call into the ctx-tree equivalent and injects it as a soft advisory: "do not call Read directly โ€” call this instead: ctx_tree_read({file_path: โ€ฆ})". Does not deny; Claude is nudged but not blocked. Useful for gradual adoption or debugging.
โœฆ soft advisory injected
PostToolUse posttooluse-agent-capture.mjs Agent tool completes
When it fires
After the Agent tool returns. Reads the state file written by the PreToolUse enrich hook to find which session spawned the subagent and when it started.
What it does
Queries all nodes created during the subagent's time window. Creates a note summary node as a child of the parent session. Emits summarizes edges from the summary to each captured subagent node. Deletes the state file. Result: ctx_tree_neighbors(sessionNodeId) surfaces the full subagent work without the parent agent managing any links.
โ—‰ silent graph update
PostToolUse posttooluse-skill-capture.mjs Skill tool completes
When it fires
After the Skill tool returns successfully with content โ‰ฅ50 chars. Skips empty or trivially short results that aren't worth caching.
What it does
Stores the full skill markdown as a note node keyed by skill://<name>. Content-hash deduplication means re-saving the same skill is a no-op. The PreToolUse recall hook will find this node on the next invocation and short-circuit disk access.
โ—‰ silent graph update
PostToolUse posttooluse-websearch-capture.mjs WebSearch tool completes
When it fires
After any WebSearch (or MCP search tool) returns. Extracts up to 5 URLs from the result. Only processes results containing recognizable URL fields (url, link, href).
What it does
Fire-and-forget: fetches each URL via ctx_tree_browse and stores it as a compact web_chunk node. Does not inject any context or block the response. The stored pages surface in future sessions via ctx_tree_search and the Agent enrichment hook's FTS pass.
โ—‰ fire-and-forget, no output
Stop stop-response-capture.mjs end of every response turn
When it fires
After Claude finishes generating each response turn. Skips re-entrant calls (stop_hook_active). Requires the ctx-tree store to be initialized and a valid transcript_path in the hook payload.
What it does
Reads the session transcript JSONL, extracts the last assistant message, and stores up to two nodes: a thinking node (if thinking blocks are present, including redacted) and a response node (text blocks concatenated). Wires the full conversation chain via follows edges (prompt โ†’ thinking โ†’ response) and causal derived_from edges. Position-based dedup via source_uri prevents double-storing on replay.
โ—‰ silent โ€” no context injected
โšก

Processing Pipeline

Walker ยท Chunker ยท Embedder ยท Indexer ยท Summarizer ยท Dedupe

๐Ÿ“ฅ
Raw Node
ingested, status=raw
โ†’
๐Ÿšถ
Walker
picks up raw nodes
in background
โ†’
โœ‚๏ธ
Chunker
semantic splits
by symbol / heading
โ†’
๐Ÿงฎ
Embedder
text-embedding-3
vector per chunk
โ†’
๐Ÿ—ƒ๏ธ
Indexer
SQLite vec_index
FTS5 full-text
โ†’
๐Ÿ“
Summarizer
LLM digest
per node
โ†’
โ™ป๏ธ
Dedupe
content-hash
near-duplicate merge
โ†’
โœ…
Indexed
status=indexed
searchable
โšก Pipeline ยท walker processes a raw node through to indexed
โ—ˆ

Node State Machine

How nodes move through raw โ†’ processing โ†’ indexed โ†’ stale โ†’ pruned

State transitions

โ—ˆ State machine ยท walker processes nodes through lifecycle
๐Ÿ“ฆ

Install

Plugin marketplace ยท hooks auto-configured ยท works with any Claude Code project

1
Add the agent marketplace
Joe's self-hosted marketplace hosts ctx-tree and other agent plugins. Register it once globally โ€” available across all your Claude Code sessions.
/plugin marketplace add joeblackwaslike/agent-marketplace
2
Install the ctx-tree plugin
Installs the MCP server, all hooks, and skills in one command. Hooks are written to ~/.claude/settings.json automatically.
/plugin install ctx-tree@agent-marketplace
3
Verify hooks are active
Start a new Claude Code session. You should see the ctx-tree context injection message on session start. Read any file โ€” it will be redirected to ctx_tree_read automatically.
ctx_tree_recent() # should return an empty list on first run
4
Requirements
ctx-tree requires Bun for the MCP server and ripgrep for the grep tool.
# macOS brew install bun ripgrep # Linux curl -fsSL https://bun.sh/install | bash apt install ripgrep # or: cargo install ripgrep