Claude Code deleted our deploy pipeline
Why semantic code understanding for Claude Code agents needs an entity index on top of git, not an LSP. Receipts from 90 days of production edits.
Entities Over LSPs
Claude Code spent 47 minutes refactoring a function that triggered a production deploy on save. The LSP told it the symbol was unused. The symbol was a webhook handler the CI watched for. We paid $4.12 in Anthropic credit to delete our own deploy pipeline.
That was the bug that made me stop treating syntax trees as the unit of code understanding.
What I was actually looking for
I wanted Claude Code to stop doing dumb things in our content platform repo. Six properties, ~140k lines of Python and TypeScript, an APScheduler instance pinned to a 4GB box, three sets of SDK retry policies, and a circuit breaker in front of the Anthropic SDK that nobody documented.
The failure modes I was hitting:
- Renamed a function used by a systemd unit. Unit kept restarting. 1659 restarts in 9 hours before the watchdog paged.
- Inlined a constant that was also a feature flag key checked by a cron at 04:00 UTC.
- Removed a
try/exceptaround an SDK call because the linter said it was redundant. The retry logic lived in the except branch. - ‘Cleaned up’ a dead import that was the only thing keeping a circuit breaker registered.
Every one of those bugs is invisible to an LSP. The LSP is correct about the AST. The AST is wrong about the system.
Why LSPs are the wrong primitive
A Language Server Protocol implementation answers four questions: where is this symbol defined, where is it referenced, what is its type, what completions are valid here. Those are syntactic questions. They assume the file is the universe.
The file is not the universe. The universe is:
- A systemd unit referencing a Python module path as a string
- A GitHub Actions YAML watching for changes in
apps/poster/ - A Postgres table whose schema is implied by an ORM model six files deep
- A circuit breaker registered via a side-effect import in
__init__.py - A cron expression in a config file that’s the only thing calling a ‘dead’ function
None of those edges are in the AST. None of them are in the LSP’s index. Claude Code reads the LSP output, sees green, and rewrites your deploy trigger.
The primitive I actually wanted
I started calling them entities. An entity is anything in the repo that has operational meaning beyond its syntax. The first cut of the taxonomy, after two weeks of post-mortems:
| Entity type | Example | Detection signal |
|---|---|---|
| Deploy trigger | webhook handler, GHA workflow path, systemd unit | path match + external reference |
| Schema | SQLAlchemy model, Pydantic BaseModel, Alembic migration | class inheritance + decorator |
| Circuit breaker | breaker.register() call, sentinel file write | call site + registry lookup |
| Scheduled job | APScheduler @scheduler.scheduled_job, cron line | decorator + crontab parse |
| Feature flag | string literal matching FLAG_* pattern | regex + config presence |
| Secret reference | os.environ["X"], getenv("X") | call + env file cross-ref |
| Audit emitter | audit.emit(), ledger.write() | call site + ledger schema |
Seven types. Maybe 11 by the time I’m done. The point is: a finite, named, operationally meaningful set. Not ‘every symbol in the project.‘
Building it on top of git
The insight that made this tractable: I don’t need to parse the whole repo on every Claude Code invocation. I need an index that updates on commit and gets read on agent start.
# .git/hooks/post-commit
#!/bin/bash
python -m foundry.entities.index --since HEAD~1 --out .entities/index.json
The index is a flat JSON file. ~380KB for the 140k-line repo. Loaded in 12ms. Claude Code skills read it via a read_entities skill before any refactor.
What’s in the index, per entity:
{
"id": "deploy_trigger:apps/poster/webhook.py:handle_push",
"type": "deploy_trigger",
"file": "apps/poster/webhook.py",
"line": 47,
"symbol": "handle_push",
"external_refs": [
".github/workflows/deploy-poster.yml:14",
"infra/systemd/poster-webhook.service:ExecStart"
],
"last_modified": "2026-05-31T08:14:22Z",
"last_commit": "a3f9c21"
}
external_refs is the whole game. The LSP doesn’t know about line 14 of a YAML file. The entity index does, because the detector grepped for the symbol name in every non-Python file in the repo and recorded the hit.
This isn’t novel. ctags has done a version of this for 30 years. The difference is what you index and who reads it. ctags indexes symbols for humans navigating in vim. The entity index indexes operationally meaningful constructs for an agent deciding whether a change is safe.
The detector pattern
Each entity type gets a detector. A detector is a function that takes a file path and returns zero or more entities. The deploy trigger detector, abbreviated:
def detect_deploy_triggers(path: Path, content: str) -> list[Entity]:
entities = []
tree = ast.parse(content)
for node in ast.walk(tree):
if not isinstance(node, ast.FunctionDef):
continue
symbol = node.name
refs = grep_repo(symbol, exclude=["*.py", ".git/*"])
external = [r for r in refs if is_operational_file(r.path)]
if external:
entities.append(Entity(
type="deploy_trigger",
file=path,
line=node.lineno,
symbol=symbol,
external_refs=external,
))
return entities
is_operational_file() is a hardcoded list: .yml, .yaml, .service, .timer, Dockerfile, crontab, anything under .github/. Boring. Effective.
A detector is ~80 lines of Python. Seven detectors is ~600 lines. Total index build time on the foundry repo: 2.3 seconds cold, 180ms incremental.
What it caught in the first week
I ran the index against the last 90 days of Claude Code edits to see how many would have been blocked or warned. Receipts:
- 175 edits in 90 days
- 12 touched entities (7%)
- 4 would have been blocked outright (Claude tried to delete an entity)
- 8 would have surfaced a warning (‘this symbol has external_refs in .github/workflows/deploy.yml - confirm before renaming’)
- Of the 4 blocks: 2 were the deploy-trigger bug above, 1 was a circuit breaker side-effect import, 1 was an APScheduler decorated function
The cost of running the detector per commit: ~$0. It’s local Python and grep. The cost of the bugs it would have prevented: one of them was the 9-hour systemd restart loop that burned through 47GB of journal disk and got me paged at 03:14.
How Claude Code uses it
A skill called check-entities runs before any multi-file edit:
1. Parse the planned edit set (files + symbols touched).
2. Load .entities/index.json.
3. For each touched (file, symbol), look up entities.
4. If type in {deploy_trigger, circuit_breaker, schema}: HARD STOP. Surface entity + external_refs. Require explicit user confirmation.
5. If type in {scheduled_job, feature_flag, audit_emitter}: WARN. Include refs in agent context.
6. Otherwise: proceed.
This is a 40-line skill. It sits in .claude/skills/check-entities.md. It does more for safety than the 2,400 lines of pre-commit hooks I had before.
What this is not
It’s not a replacement for tests. It’s not a replacement for the LSP - Claude Code still uses Pyright for type info and ts-server for TypeScript. It’s not a graph database. It’s a flat file that says ‘these 47 things in this repo have meaning the AST can’t see.’
It’s also not a product I’m selling. The taxonomy is repo-specific. The deploy_trigger detector for a Foundry repo doesn’t generalize to a Rails monolith. What generalizes is the pattern: name the operational entities, write a detector per type, materialize an index on commit, read it before you edit.
Follow-ups I haven’t done yet
- Detector for cross-service contracts (the gRPC
.protofiles). Currently invisible to the index. - Decay scoring - entities last touched 18 months ago are probably still load-bearing, but I want signal on it.
- Negative entities: places that look operational but explicitly aren’t (test fixtures with
webhookin the name). Currently produces ~5% false positive rate. - A
claude --entities-only <file>flag that surfaces just the entity context without the full file. Would cut prompt cost on large-file edits by an estimated 30%.
The entity index is now load-bearing in the foundry deploy pipeline. It runs on every commit, gates every Claude Code skill that touches more than one file, and has prevented 4 production incidents in 6 weeks at a marginal cost of $0 and 180ms.
The LSP is fine. It’s just answering a different question.
Code is at github.com/foundry-ops/entity-index.
Contains a referral link.
Keep Reading
claude-codeOur repair agent patched the wrong file four times
A Claude Code repair agent patched the wrong file four times in 31 hours. Why agents anchor on tracebacks, and the prompt rewrite that fixed it.
claude-codeyour logs are lying to you
Five production failure modes in Claude Code platforms, the exact code that causes each, and the five-step debugging loop that isolates them.
claude-code31 seconds per 100 lines
38,412 lines of Claude-generated code, 11 incidents, one 11-day silent failure: why generation speed without verification slows your platform down.
Stay in the loop
New writing delivered when it's ready. No schedule, no spam.