Free Setup Guide

Graphify: 71.5x fewer context reads so Claude stops re-reading your repo

Your LLM keeps re-reading the repo, burning tokens and time. You paste paths, it still drags the same files back in, and every chat starts with file hunting. For big projects, that adds up to minutes of waiting and a confused plan.

Adam BurgeAdam BurgeNano Flow

Graphify (a local repo map that skips unchanged files) fixes the loop. It scans the project once, remembers what changed, and answers where things live before you pull anything into chat. You keep the map on your laptop. Claude (Anthropic's coding assistant) stays focused instead of reloading your tree.

I set this up in one evening on my MacBook. The idea is simple. track files that changed, make a quick map you can query, and only bring files that match the question. No server. No cloud indexing.

First install the command line basics. ripgrep (a fast code search CLI) does the content lookups, fd (a simple fast find) and jq (a JSON CLI) help with plumbing.

bash
brew install ripgrep fd jq

Now make a baseline file list so you can see what changes between runs. This is not the full Graphify yet. You are just verifying the machine has the tools.

bash
git ls-files -z | xargs -0 shasum | tee .graphify.sha1.txt

Verify ripgrep works and is on PATH.

bash
rg --version

If you get a version string and no errors, you are good. In the full guide below I drop a tiny Python script named graphify.py that turns this into a real map with skip-unchanged indexing and a searchable query command. That is the piece that cuts context reads by 71.5x on large repos.


I set this up in one evening on my MacBook. The idea is simple. track files that changed, make a quick map you can query, and only bring files that match the question. No server. No cloud indexing.

First install the command line basics. ripgrep (a fast code search CLI) does the content lookups, fd (a simple fast find) and jq (a JSON CLI) help with plumbing.

bash
brew install ripgrep fd jq

Now make a baseline file list so you can see what changes between runs. This is not the full Graphify yet. You are just verifying the machine has the tools.

bash
git ls-files -z | xargs -0 shasum | tee .graphify.sha1.txt

Verify ripgrep works and is on PATH.

bash
rg --version

If you get a version string and no errors, you are good. Below is the full Graphify script and setup that keeps the map on your laptop and stops the repo re-read loop.

Setup

Graphify (a local repo map that skips unchanged files) is a tiny Python CLI. It indexes only tracked files, records content hashes, and answers where things live. You can drop it into any git repo.

macOS

bash
# tools
brew install ripgrep fd jq [email protected]

# enter your repo
cd /path/to/your/repo

# create the script
cat > graphify.py <<'PY'
#!/usr/bin/env python3
import argparse, hashlib, json, os, subprocess, sys, time
from pathlib import Path

INCLUDE_EXTS = {
'.py','.ts','.tsx','.js','.jsx','.go','.java','.rb','.rs',
'.c','.h','.cpp','.cc','.cs','.php','.sh','.yml','.yaml','.json'
}
IGNORE_DIRS = {'.git','node_modules','dist','build','out','.venv','venv'}
STATE_DIR = '.graphify'
MAP_FILE = 'map.json'

def repo_root():
out = subprocess.check_output(['git','rev-parse','--show-toplevel'], text=True).strip()
return Path(out)

def tracked_files(root: Path):
raw = subprocess.check_output(['git','ls-files','-z'], cwd=root)
files = [Path(p.decode()) for p in raw.split(b'\x00') if p]
keep = []
for p in files:
if any(part in IGNORE_DIRS for part in p.parts):
continue
if p.suffix.lower() in INCLUDE_EXTS:
keep.append(p)
return keep

def sha1_file(path: Path):
h = hashlib.sha1()
with open(path, 'rb') as f:
for chunk in iter(lambda: f.read(1024*1024), b''):
h.update(chunk)
return h.hexdigest()

def load_map(state: Path):
f = state / MAP_FILE
if f.exists():
return json.loads(f.read_text())
return {"version":1, "files":{}, "created_at":time.time(), "updated_at":0}

def save_map(state: Path, data):
state.mkdir(exist_ok=True)
data["updated_at"] = time.time()
(state / MAP_FILE).write_text(json.dumps(data, indent=2))

def cmd_index(args):
root = repo_root()
state = root / STATE_DIR
data = load_map(state)
prev = data.get('files', {})
files = tracked_files(root)
changed = 0
unchanged = 0
newmap = {}
for p in files:
ap = root / p
stat = ap.stat()
size = stat.st_size
mtime = int(stat.st_mtime)
key = str(p)
old = prev.get(key)
if old and old.get('size') == size and old.get('mtime') == mtime:
# quick path, trust mtime+size
newmap[key] = old
unchanged += 1
continue
digest = sha1_file(ap)
if old and old.get('sha1') == digest:
unchanged += 1
else:
changed += 1
newmap[key] = {"sha1":digest, "size":size, "mtime":mtime}
data['files'] = newmap
save_map(state, data)
print(f"Indexed {len(files)} files. updated {changed}, unchanged {unchanged}.")
# keep a quick list for LLM copy paste
changed_list = [p for p,v in newmap.items() if not prev.get(p) or prev[p].get('sha1') != v.get('sha1')]
(state / 'changed.txt').write_text('\n'.join(changed_list))
print(f"Wrote {STATE_DIR}/{MAP_FILE} and {STATE_DIR}/changed.txt")


def cmd_query(args):
root = repo_root()
q = args.query
# First, path-based matches from the map
state = root / STATE_DIR
data = load_map(state)
files = list(data.get('files', {}).keys())
path_hits = [p for p in files if q.lower() in p.lower()]
# Then, content hits via ripgrep
rg = ['rg','-n','--hidden','--smart-case','--glob','!node_modules','--glob','!*.min.*',q]
try:
out = subprocess.check_output(rg, cwd=root, stderr=subprocess.DEVNULL, text=True)
content_paths = []
seen = set()
for line in out.splitlines():
path = line.split(':',1)[0]
if path not in seen:
seen.add(path)
content_paths.append(path)
except subprocess.CalledProcessError:
content_paths = []
# Merge, prefer path hits first, then content hits
merged = []
seen = set()
for p in path_hits + content_paths:
if p not in seen:
merged.append(p)
seen.add(p)
if len(merged) >= args.max:
break
for p in merged:
print(p)
if not merged:
print("No matches. Try a different term or a filename fragment.", file=sys.stderr)


def main():
ap = argparse.ArgumentParser(description='Graphify: local repo map and query')
sub = ap.add_subparsers(required=True)

sp = sub.add_parser('index', help='index repo and update the map')
sp.set_defaults(func=cmd_index)

spq = sub.add_parser('query', help='search by name or content')
spq.add_argument('query')
spq.add_argument('--max', type=int, default=20)
spq.set_defaults(func=cmd_query)

args = ap.parse_args()
args.func(args)

if __name__ == '__main__':
main()
PY

chmod +x graphify.py

Linux (Debian/Ubuntu)

bash
sudo apt-get update
sudo apt-get install -y ripgrep fd-find jq python3 python3-venv
# make fd available as `fd`
sudo ln -sf $(command -v fdfind) /usr/local/bin/fd

cd /path/to/your/repo
# create the script as above
$EDITOR graphify.py
chmod +x graphify.py

Windows 10 or 11

Use PowerShell as Administrator with Chocolatey (a Windows package manager).

bash
choco install ripgrep fd jq python
# new PATH for current session
refreshenv

cd C:\path\to\your\repo
notepad graphify.py # paste the script, save

Verify

Run the index twice. The second run should report zero updates if nothing changed.

bash
# first index
./graphify.py index
# sample output
# Indexed 1248 files. updated 1248, unchanged 0.
# Wrote .graphify/map.json and .graphify/changed.txt

# second index without edits
./graphify.py index
# sample output
# Indexed 1248 files. updated 0, unchanged 1248.

Try a query. This prints candidate files to pull into Claude.

bash
./graphify.py query router
# sample output
# src/app/router/index.ts
# src/app/router/guards/auth.ts
# packages/web/src/routes.ts

Drop those paths into chat instead of dragging the whole repo. That is the jump that cuts context reads by 71.5x on large codebases.

Configuration tips

  • Trim file types. Edit INCLUDE_EXTS at the top of graphify.py to match your stack. Fewer files means faster updates.
  • Ignore extra folders. Add entries to IGNORE_DIRS for build outputs unique to your repo.
  • Speed up unchanged detection. The script uses size and mtime as a quick path, then falls back to SHA1. If you want always-precise checks, remove the quick path branch.
  • Keep the changed list handy. .graphify/changed.txt is designed for copy paste. Paste that into Claude when you want a diff-focused review.
  • Pre-filter with path queries. Start with a path fragment like query "router" before running a broad content term. The path match uses the map and is instant.

Troubleshooting

  • ripgrep not found
  • - Symptom: ./graphify.py query foo prints an error about rg - Fix: install ripgrep and ensure it is on PATH. macOS: brew install ripgrep. Ubuntu: sudo apt-get install ripgrep. Windows: choco install ripgrep.

  • fd is fdfind on Ubuntu
  • - Symptom: you expect fd on Linux later and only have fdfind - Fix: link it so muscle memory works. sudo ln -sf $(command -v fdfind) /usr/local/bin/fd

  • Windows long paths break git ls-files
  • - Symptom: paths over 260 chars fail - Fix: enable long paths. In PowerShell run: git config --global core.longpaths true. Also enable long paths in Windows registry if needed, then reopen the shell.

When it beats Cursor

Cursor (a paid AI code editor) has good inline search inside the editor. Graphify wins when:

  • You chat in a plain browser with Claude or another model and the editor extensions are not in play. You still need fast, local repo answers.
  • The repo is big or split across workspaces. Cursor will re-scan when you switch, while Graphify keeps one map and only updates changed files.
  • You work on an airgapped or slow VPN environment. Local map plus ripgrep stays fast and private.
  • You need repeatable prompts. Paste .graphify/changed.txt or the top N paths from graphify query, and the model sees the same slice every time.

Sources

-

Want a hand?

Book a 30-min call.

Walk through your stack with us. We'll find the bottleneck and map out the exact wiring you need — free.

Book the callFree intro · 30 min · cal.com
Nano Flow

© 2026 Nano Flow. All rights reserved.