{"slug": "a-code-knowledge-graph-cut-claude-codes-token-use-25-heres-how-to-build-one", "title": "A Code Knowledge Graph Cut Claude Code’s Token Use 25%. Here’s How to Build One", "summary": "A builder at FalkorDB's Graph Hacks hackathon (October 15–18, 2026, $20K in prizes, 7,899 developers and 1,213 projects) connected Claude Code to a FalkorDB code knowledge graph of a real repository and cut input token use by 25% and tool calls by 29% across 72 runs. A second builder used the same graph approach to select about 36 of click's 2,209 tests per change, catching 30 of 31 planted bugs. The tutorial walks through loading a Python repo into a free FalkorDB Cloud instance in about 80 lines of Python, ranking load-bearing functions with algo.pageRank, and connecting Claude Code through the FalkorDB MCP server in strict read-only mode.", "body_md": "Coding agents spend a lot of their time searching: grepping for callers, opening files to find the right tests, rereading code they already saw. At Graph Hacks, FalkorDB's online hackathon (October 15 to 18, 2026, $20K in prizes), which drew 7,899 developers and 1,213 projects, one builder gave Claude Code a FalkorDB map of a real repo. Across 72 runs it used 25% fewer input tokens and 29% fewer tool calls. Another builder used the same kind of graph to pick about 36 of click's 2,209 tests per change and still caught 30 of 31 planted bugs. Here's how to build a code knowledge graph on a free FalkorDB Cloud instance and connect it to Claude Code, in six steps.\n\n**Key takeaways.** A code knowledge graph lets an agent ask what a change touches before it edits. In six steps you load a real Python repo into FalkorDB with about 80 lines of Python, ask three read-only questions, rank load-bearing functions with\n\n`algo.pageRank`, and connect Claude Code through the FalkorDB MCP server in strict read-only mode. Every snippet was run against FalkorDB, and the output shown is real.\n**Follow along for free.** Every step runs on a free FalkorDB Cloud instance, with no credit card. [Start free on FalkorDB Cloud](https://app.falkordb.cloud/signup?cta=cta-top) and keep the tab open for step 1.\n\n## What is a code knowledge graph?\n\nA code knowledge graph stores a repository's files, functions and tests as nodes, and calls, test coverage and co-changes as edges, so an agent can ask what a change touches.\n\nClaude Code is great at reading and editing files, but files don't show the structure between them. In 83 of Graph Hacks builder Manoj Shetty's own Claude Code sessions, 31% of 32,713 tool calls were spent searching or navigating. A graph answers those questions with one query each.\n\n| Question before an edit | Text search | Code graph on FalkorDB | \n|---|---|---|\n| What calls this function? | Grep, then read every hit | One `CALLS*1..3` query | \n| Which tests reach it? | Guess from file names | Walk back from the function | \n| What changes with this file? | Read `git log` by hand | `CO_CHANGED` edges | \n| Which code is load-bearing? | No direct answer | `algo.pageRank` | \n\n**What you'll need.** Python 3, git, Node.js (for\n\n`npx`), Claude Code, and a free FalkorDB Cloud account.\n**The six steps.**\n\n[1. Create a free Cloud instance](#step-1)·\n\n[2. Pick a repo](#step-2)·\n\n[3. Load the graph](#step-3)·\n\n[4. Ask questions](#step-4)·\n\n[5. Rank with PageRank](#step-5)·\n\n[6. Connect Claude Code](#step-6)\n\n## Step 1. Create a free FalkorDB Cloud instance\n\n[Create your free FalkorDB Cloud instance](https://app.falkordb.cloud/signup?cta=tutorial-step1)\n\nNo credit card needed. In the console, open Deployments, then Instances, and select Create. Pick the FalkorDB Free plan, a provider and a region, and set a password. When the instance shows Running, copy the host and port from its Connectivity tab. The port is often not 6379, and the username is `falkordb`. A Free instance stops after 1 day without use and is deleted after 7 days, so re-run `load.py` if your graph is gone.\n\n```\nexport FALKORDB_HOST=\"<host>\"\nexport FALKORDB_PORT=\"<port>\"\nexport FALKORDB_USERNAME=\"falkordb\"\nexport FALKORDB_PASSWORD=\"<password>\"\n```\n\n## Step 2. Pick a repo for your code knowledge graph\n\nWe'll map pallets/itsdangerous, a small library from the team behind Flask and click, with 8 source files and 677 commits at the time of writing. Any Python repo works. Clone the full history, because co-change comes from `git log`.\n\n```\npip install falkordb\ngit clone https://github.com/pallets/itsdangerous\n```\n\n## Step 3. Model the code knowledge graph and load it into FalkorDB\n\n| Edge | Connects | Source | \n|---|---|---|\n| `DEFINES` | File to Function | Python's `ast` module | \n| `CALLS` | Function to Function | Calls in the body, matched by name | \n| `COVERS` | Test to Function | Calls in each `test_` function | \n| `CO_CHANGED` | File to File | Edited together in 3 or more commits | \n\nSave this as `load.py` next to the `itsdangerous` folder and run `python load.py`. It parses the code without running it, reads the git history, and loads everything in batches with `UNWIND`. It rebuilds the graph named `code` on every run.\n\n``` python\nimport ast, os, subprocess\nfrom collections import Counter\nfrom itertools import combinations\nfrom pathlib import Path\nfrom falkordb import FalkorDB\n\nREPO = Path(\"itsdangerous\")\n\ndef functions(path):\n    \"\"\"Every function and method in a file, with its qualified name.\"\"\"\n    def walk(node, prefix):\n        for child in ast.iter_child_nodes(node):\n            if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):\n                yield prefix + child.name, child\n            elif isinstance(child, ast.ClassDef):\n                yield from walk(child, prefix + child.name + \".\")\n    yield from walk(ast.parse(path.read_text()), \"\")\n\ndef called_names(fn):\n    \"\"\"Names a function calls: foo() and obj.foo() both give 'foo'.\"\"\"\n    for n in ast.walk(fn):\n        if isinstance(n, ast.Call):\n            if isinstance(n.func, ast.Name):\n                yield n.func.id\n            elif isinstance(n.func, ast.Attribute):\n                yield n.func.attr\n\nfiles, funcs, tests, calls, covers = [], {}, {}, set(), set()\nfor path in sorted(REPO.glob(\"src/**/*.py\")):\n    rel = str(path.relative_to(REPO))\n    files.append(rel)\n    for qualname, fn in functions(path):\n        fid = f\"{rel}::{qualname}\"\n        funcs[fid] = {\"id\": fid, \"name\": fn.name, \"file\": rel}\n        calls |= {(fid, name) for name in called_names(fn)}\nfor path in sorted(REPO.glob(\"tests/**/test_*.py\")):\n    rel = str(path.relative_to(REPO))\n    for qualname, fn in functions(path):\n        if fn.name.startswith(\"test_\"):\n            tid = f\"{rel}::{qualname}\"\n            tests[tid] = {\"id\": tid, \"file\": rel}\n            covers |= {(tid, name) for name in called_names(fn)}\n\n# Co-change: pairs of source files edited in the same commit, counted over git history.\nlog = subprocess.run([\"git\", \"-C\", str(REPO), \"log\", \"--name-only\", \"--pretty=format:@@\"],\n                     capture_output=True, text=True, check=True).stdout\npairs = Counter()\nfor commit in log.split(\"@@\"):\n    changed = sorted({p for p in commit.splitlines() if p in files})\n    pairs.update(combinations(changed, 2))\n\ndb = FalkorDB(host=os.environ[\"FALKORDB_HOST\"], port=int(os.environ[\"FALKORDB_PORT\"]),\n              username=os.environ[\"FALKORDB_USERNAME\"], password=os.environ[\"FALKORDB_PASSWORD\"])\nif \"code\" in db.list_graphs():\n    db.select_graph(\"code\").delete()\ng = db.select_graph(\"code\")\ng.query(\"CREATE INDEX FOR (f:Function) ON (f.id, f.name)\")\ng.query(\"CREATE INDEX FOR (f:File) ON (f.path)\")\ng.query(\"CREATE INDEX FOR (t:Test) ON (t.id)\")\n\ndef load(cypher, rows):\n    g.query(\"UNWIND $rows AS r \" + cypher, {\"rows\": rows})\n\nload(\"CREATE (:File {path: r})\", files)\nload(\"\"\"MATCH (file:File {path: r.file})\n        CREATE (file)-[:DEFINES]->(:Function {id: r.id, name: r.name, file: r.file})\"\"\",\n     list(funcs.values()))\nload(\"CREATE (:Test {id: r.id, file: r.file})\", list(tests.values()))\n# Calls are resolved by name, so this is an approximate call graph. Dunder names are skipped.\nload(\"\"\"MATCH (a:Function {id: r.src}), (b:Function {name: r.name})\n        WHERE a <> b AND NOT r.name STARTS WITH '__' CREATE (a)-[:CALLS]->(b)\"\"\",\n     [{\"src\": s, \"name\": n} for s, n in calls])\nload(\"\"\"MATCH (t:Test {id: r.src}), (f:Function {name: r.name})\n        WHERE NOT r.name STARTS WITH '__' CREATE (t)-[:COVERS]->(f)\"\"\",\n     [{\"src\": s, \"name\": n} for s, n in covers])\nload(\"\"\"MATCH (a:File {path: r.a}), (b:File {path: r.b})\n        CREATE (a)-[:CO_CHANGED {commits: r.n}]->(b)\"\"\",\n     [{\"a\": a, \"b\": b, \"n\": n} for (a, b), n in pairs.items() if n >= 3])\n\nprint(g.query(\"MATCH (n) RETURN labels(n)[0] AS label, count(n) ORDER BY label\").result_set)\nprint(g.query(\"MATCH ()-[r]->() RETURN type(r) AS type, count(r) ORDER BY type\").result_set)\n[['File', 8], ['Function', 54], ['Test', 37]]\n[['CALLS', 109], ['COVERS', 185], ['CO_CHANGED', 22], ['DEFINES', 54]]\n```\n\nThat's 99 nodes and 370 edges. Calls are matched by name, so the call graph is approximate, which is fine for steering an agent. For exact `COVERS` edges, record per-test coverage, as ImpactGraph does below. The queries stay the same.\n\n## Step 4. Ask the code knowledge graph three read-only questions\n\nSay you're about to change `Signer.derive_key`. What calls it, which tests reach it, and what usually changes with `signer.py`? Save this as `ask.py` and run it.\n\n``` python\nimport os\nfrom falkordb import FalkorDB\n\ndb = FalkorDB(host=os.environ[\"FALKORDB_HOST\"], port=int(os.environ[\"FALKORDB_PORT\"]),\n              username=os.environ[\"FALKORDB_USERNAME\"], password=os.environ[\"FALKORDB_PASSWORD\"])\ng = db.select_graph(\"code\")\n\nWHAT_CALLS = \"\"\"\nMATCH p = (caller:Function)-[:CALLS*1..3]->(:Function {name: $name})\nRETURN caller.id AS caller, min(length(p)) AS hops\nORDER BY hops, caller LIMIT 5\"\"\"\n\nWHICH_TESTS = \"\"\"\nMATCH p = (t:Test)-[:COVERS]->(:Function)-[:CALLS*0..2]->(:Function {name: $name})\nRETURN t.id AS test, min(length(p)) AS hops\nORDER BY hops, test LIMIT 5\"\"\"\n\nCHANGES_WITH = \"\"\"\nMATCH (:File {path: $path})-[c:CO_CHANGED]-(other:File)\nRETURN other.path AS file, c.commits AS commits\nORDER BY commits DESC LIMIT 3\"\"\"\n\nfor title, query, params in [\n    (\"What calls derive_key?\", WHAT_CALLS, {\"name\": \"derive_key\"}),\n    (\"Which tests reach derive_key?\", WHICH_TESTS, {\"name\": \"derive_key\"}),\n    (\"What changes with signer.py?\", CHANGES_WITH, {\"path\": \"src/itsdangerous/signer.py\"}),\n]:\n    print(title)\n    for row in g.ro_query(query, params).result_set:\n        print(\"  \", *row)\nWhat calls derive_key?\n   src/itsdangerous/signer.py::Signer.get_signature 1\n   src/itsdangerous/signer.py::Signer.verify_signature 1\n   src/itsdangerous/signer.py::Signer.sign 2\n   src/itsdangerous/signer.py::Signer.unsign 2\n   src/itsdangerous/signer.py::SigningAlgorithm.verify_signature 2\nWhich tests reach derive_key?\n   tests/test_itsdangerous/test_signer.py::TestSigner.test_invalid_key_derivation 1\n   tests/test_itsdangerous/test_signer.py::TestSigner.test_broken_signature 2\n   tests/test_itsdangerous/test_signer.py::test_abstract_algorithm 2\n   tests/test_itsdangerous/test_serializer.py::TestSerializer.test_bad_payload_exception 3\n   tests/test_itsdangerous/test_serializer.py::TestSerializer.test_loads_unsafe 3\nWhat changes with signer.py?\n   src/itsdangerous/serializer.py 10\n   src/itsdangerous/exc.py 6\n   src/itsdangerous/timed.py 6\n```\n\n`CALLS*1..3` finds callers two hops away, like `Signer.sign`, that a name search would miss. The second query finds tests that reach the function through other code, ranked by distance. And `ro_query` sends `GRAPH.RO_QUERY`, which FalkorDB runs read-only on the server. A `CREATE` sent through it was refused.\n\n## Step 5. Find load-bearing code in the code knowledge graph with PageRank\n\nA change to code that everything else depends on needs the widest test run. FalkorDB runs PageRank inside the database, even in a read-only query.\n\n``` python\nimport os\nfrom falkordb import FalkorDB\n\ndb = FalkorDB(host=os.environ[\"FALKORDB_HOST\"], port=int(os.environ[\"FALKORDB_PORT\"]),\n              username=os.environ[\"FALKORDB_USERNAME\"], password=os.environ[\"FALKORDB_PASSWORD\"])\ng = db.select_graph(\"code\")\n\nLOAD_BEARING = \"\"\"\nCALL algo.pageRank('Function', 'CALLS') YIELD node, score\nRETURN node.id AS function, round(score * 1000) / 1000.0 AS score\nORDER BY score DESC LIMIT 5\"\"\"\n\nfor row in g.ro_query(LOAD_BEARING).result_set:\n    print(*row)\nsrc/itsdangerous/encoding.py::want_bytes 0.081\nsrc/itsdangerous/serializer.py::Serializer.make_signer 0.027\nsrc/itsdangerous/serializer.py::Serializer._loads_unsafe_impl 0.022\nsrc/itsdangerous/serializer.py::Serializer.dumps 0.021\nsrc/itsdangerous/serializer.py::_PDataSerializer.dumps 0.021\n```\n\n`want_bytes` ranks first. 30 functions reach it within three calls, because almost every encode and decode path goes through it. An agent should know that before it touches a helper that looks small.\n\n## Step 6. Connect Claude Code to the code knowledge graph through the FalkorDB MCP server\n\nRegister the official FalkorDB MCP server from inside your project, with the values from step 1. `FALKORDB_DEFAULT_READONLY` runs queries with `GRAPH.RO_QUERY`, and `FALKORDB_STRICT_READONLY` blocks writes even if the agent asks for one.\n\n```\nclaude mcp add falkordb \\\n  -e FALKORDB_HOST=$FALKORDB_HOST -e FALKORDB_PORT=$FALKORDB_PORT \\\n  -e FALKORDB_USERNAME=$FALKORDB_USERNAME -e FALKORDB_PASSWORD=$FALKORDB_PASSWORD \\\n  -e FALKORDB_DEFAULT_READONLY=true -e FALKORDB_STRICT_READONLY=true \\\n  -- npx -y @falkordb/mcpserver\nclaude mcp get falkordb\nfalkordb:\n  Scope: Local config (private to you in this project)\n  Status: √ Connected\n  Type: stdio\n  Command: npx\n  Args: -y @falkordb/mcpserver\n```\n\nWe tested server version 1.3.0 against the step 3 graph. The step 4 callers query returned the same rows through its `query_graph_readonly` tool, and a write came back with \"Cannot execute write queries: server is in strict read-only mode\".\n\nThen tell Claude Code the graph exists, with a short section in your project's `CLAUDE.md`.\n\n``` php\n## Code graph\nThe FalkorDB graph named code maps this repo.\n(:File)-[:DEFINES]->(:Function), (:Function)-[:CALLS]->(:Function),\n(:Test)-[:COVERS]->(:Function), (:File)-[:CO_CHANGED {commits}]->(:File)\nBefore you change a function, use the falkordb tools to find its callers\nand the tests that reach it. Run those tests after the edit.\n```\n\nNow ask \"What could break if I change `Signer.derive_key`, and which tests should I run?\" Claude Code can look up callers and tests through `query_graph_readonly` instead of a round of searching. Re-run `load.py` from a git hook to stay current. For a ready-made version, FalkorDB's open source [CodeGraph](https://github.com/FalkorDB/code-graph) indexes Python, Java and C# repos and ships its own MCP server.\n\n## What did a code knowledge graph change on pallets/click?\n\n### ImpactGraph runs only the tests a change can break\n\n**ImpactGraph** · Manoj Kengudelu · [See the code](https://github.com/manojkken/impactgraph)\n\nMost of click's 2,209 tests never execute a given change, and picking tests by file name misses the ones that reach the code through other functions.\n\nImpactGraph keeps click's code, tests and history in one FalkorDB graph of 545 functions, 2,209 tests, 360 commits and about 157,600 edges, 152,526 of them per-test `COVERS` edges from real coverage data. A rule-based ranker and a Claude agent pick tests only through fixed `GRAPH.RO_QUERY` queries, and every pick comes with the graph path to the change. It runs on embedded FalkorDBLite or on any FalkorDB server, including FalkorDB Cloud.\n\n| Setup (31 planted bugs) | Tests run per change | Bugs caught | \n|---|---|---|\n| Full suite | 2,209 | 31 | \n| ImpactGraph on FalkorDB | 35.5 on average | 30 | \n| Random tests, same number | 35.5 on average | 12 | \n\nThe selection ran in 0.4 seconds, against 5.9 for the full suite. 83 functions in click have no test, and for 9 of them this query still finds tests through covered callers.\n\n```\nUNWIND $changed AS fid\nMATCH (f:Function {id: fid}) WHERE f.tests = 0\nMATCH p = (caller:Function)-[:CALLS*1..3]->(f)\nWHERE caller.tests > 0\nWITH f, caller, min(length(p)) AS hops\nMATCH (t:Test)-[:COVERS]->(caller)\nRETURN f.id AS function, t.id AS test, t.file AS file, t.breadth AS breadth,\n       caller.qualname AS via, hops\n```\n\n**Try it.** Swap the name-matched `COVERS` edges from step 3 for per-test coverage, and keep the step 4 queries. [Start free on FalkorDB Cloud](https://app.falkordb.cloud/signup?cta=impactgraph).\n\n### Engram gives Claude Code a map before the first tool call\n\n**Engram** · Manoj Shetty · [See the code](https://github.com/ManojRakshu/engram)\n\nA coding agent greps, reads and greps again until it finds the function, and next session it starts from zero.\n\nEngram turns each repo into its own FalkorDB graph of symbols, calls, commits, co-changes and the agents' own sessions. Before Claude Code's first tool call, it adds a map of about 200 tokens with the likeliest `file:lines`, from 20 to 90 ms of graph queries and no LLM calls. It uses `algo.pageRank`, `algo.labelPropagation` and full-text indexes, and every agent-facing read goes through `GRAPH.RO_QUERY`.\n\nAcross 72 Claude Code runs on 12 click bugs with the same model (36 with Engram, 36 without), Engram cut input tokens by 25%, tool calls by 29% and time by 23%. Search calls fell from 3.4 to 1.7 per bug. Both setups fixed nearly every bug (35 of 36 with Engram, 36 of 36 without). After every edit, this query joins git co-change, the session's edits and the call graph in about 11 ms to name files the agent probably also needs to change.\n\n```\nMATCH (f:File {path: $file})-[k:CO_CHANGES]-(o:File)\nWHERE k.count >= 3 AND k.weight >= 0.3 AND NOT o.isTest AND NOT o.isAux\nOPTIONAL MATCH (:Session {id: $sid})-[e:EDITED]->(o)\nWITH f, o, k, e WHERE e IS NULL\nOPTIONAL MATCH (o)-[:DEFINES]->(c:Symbol)-[:CALLS]->(:Symbol {path: $file})\nWITH o, k, collect(DISTINCT c.qualname)[0..2] AS dependents\nWHERE size(dependents) > 0 OR k.weight >= 0.5\nRETURN o.path AS other, k.count AS count, k.weight AS weight, dependents\nORDER BY size(dependents) DESC, k.count DESC LIMIT 2\n```\n\nEngram runs embedded FalkorDBLite on each machine. For a team, its docs say to point everyone at one FalkorDB, such as FalkorDB Cloud.\n\n**Try it.** Put your team's code graph on one FalkorDB Cloud instance, and every agent reads the same map. [Start free on FalkorDB Cloud](https://app.falkordb.cloud/signup?cta=engram).\n\nWant to go further? Read [5 agent memory patterns from 647 Graph Hacks projects](https://www.falkordb.com/blog/ai-agent-memory-graph-patterns/), or see how teams built [a company brain their agents can query](https://www.falkordb.com/blog/company-brain-ai-agents-graph/).\n\n## Frequently asked questions\n\n### What is a code knowledge graph?\n\nA code knowledge graph stores a repository's files, functions and tests as nodes, and calls, test coverage and co-changes as edges, so an agent can ask what a change touches. Questions like what calls this function or which tests reach it take one query each.\n\n### How do I connect Claude Code to a graph database?\n\nUse an MCP server. For FalkorDB, run `claude mcp add falkordb` with your connection details as `-e` variables, then `-- npx -y @falkordb/mcpserver`. Add `FALKORDB_STRICT_READONLY=true` so the agent can only read. See the [FalkorDB MCP server docs](https://docs.falkordb.com/genai-tools/mcpserver/).\n\n### Does a code knowledge graph help coding agents?\n\nIn a Graph Hacks benchmark of 72 Claude Code runs on pallets/click, a FalkorDB code graph cut input tokens by 25% and tool calls by 29% with the same model. In another, a FalkorDB graph of click picked about 36 of 2,209 tests per change and caught 30 of 31 planted bugs.\n\n### Can I build a code knowledge graph on the FalkorDB free tier?\n\nYes. Every step in this tutorial runs on a free FalkorDB Cloud instance, with no credit card. The example graph of pallets/itsdangerous has 99 nodes and 370 edges.\n\n### How do I keep a code knowledge graph up to date?\n\nRe-run the loader when code changes, for example from a git hook or CI. Larger tools re-index only the files that changed.\n\n## Give Claude Code a code knowledge graph of your repo\n\nEverything in this tutorial runs on the same FalkorDB you can start with one Docker command, `docker run -p 6379:6379 -p 3000:3000 -it --rm falkordb/falkordb:latest`, or a free cloud instance. Load one repo and let your agent ask what a change touches before every edit.\n\n[Start free on FalkorDB Cloud](https://app.falkordb.cloud/signup?cta=cta-bottom)\n\n[Read the get started guide](https://docs.falkordb.com/#get-started)\n\nNot ready to build yet? [Join the FalkorDB Discord](https://discord.gg/Y2mMT9VdMy) to get help from our engineers, share what you build, and hear about the next Graph Hacks first.", "url": "https://wpnews.pro/news/a-code-knowledge-graph-cut-claude-codes-token-use-25-heres-how-to-build-one", "canonical_source": "https://www.falkordb.com/blog/code-knowledge-graph-claude-code/", "published_at": "2026-11-03 17:05:50+00:00", "updated_at": "2026-11-03 17:20:23.141177+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "agent-protocols", "developer-tools", "artificial-intelligence"], "entities": ["FalkorDB", "Claude Code", "Graph Hacks", "Manoj Shetty", "pallets/itsdangerous", "click", "Flask", "FalkorDB Cloud"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/a-code-knowledge-graph-cut-claude-codes-token-use-25-heres-how-to-build-one", "markdown": "https://wpnews.pro/news/a-code-knowledge-graph-cut-claude-codes-token-use-25-heres-how-to-build-one.md", "text": "https://wpnews.pro/news/a-code-knowledge-graph-cut-claude-codes-token-use-25-heres-how-to-build-one.txt", "jsonld": "https://wpnews.pro/news/a-code-knowledge-graph-cut-claude-codes-token-use-25-heres-how-to-build-one.jsonld"}}