RC RANDOM CHAOS

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.

· 6 min read

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/except around 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 typeExampleDetection signal
Deploy triggerwebhook handler, GHA workflow path, systemd unitpath match + external reference
SchemaSQLAlchemy model, Pydantic BaseModel, Alembic migrationclass inheritance + decorator
Circuit breakerbreaker.register() call, sentinel file writecall site + registry lookup
Scheduled jobAPScheduler @scheduler.scheduled_job, cron linedecorator + crontab parse
Feature flagstring literal matching FLAG_* patternregex + config presence
Secret referenceos.environ["X"], getenv("X")call + env file cross-ref
Audit emitteraudit.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 .proto files). 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 webhook in 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.

Share

Keep Reading

Stay in the loop

New writing delivered when it's ready. No schedule, no spam.