DEV Community

runathicku
runathicku

Posted on

What I learned rolling out Claude Code across a company full of legacy systems

I maintain the internal systems of a mid-size manufacturer in Japan: pre-framework PHP that predates Composer, VB.NET ClickOnce apps nobody has recompiled in years, and a shared Oracle schema whose only documentation is the schema itself. I've been running Claude Code against this estate daily for months, and rolled it out to colleagues.

Modern-stack advice ("just run the tests!") doesn't survive contact with this world. Here is what actually worked.

1. Hooks are the only guardrail that actually holds

CLAUDE.md rules are suggestions. Under context pressure - long sessions, compacted history - the model can drift from them. A PreToolUse hook cannot drift: it's a program that inspects every shell command before execution and forces a human approval prompt when it matches a dangerous pattern.

Ours escalates:

  • SQL write verbs reaching any DB CLI: INSERT/UPDATE/DELETE/DROP/TRUNCATE/ALTER through mysql, psql, sqlplus, sqlcmd - including piped .sql files, whose contents you can't see at hook time
  • rm -rf, git push --force, git reset --hard, curl | sh
  • service restarts and registry writes on Windows

The mechanism is simple: the hook reads the tool call as JSON on stdin, regex-scans the command, and returns a permissionDecision of "ask" with a reason. Twenty lines of bash or PowerShell. The key design decision: ask, not deny - legitimate destructive work still happens, it just requires a human to say yes.

2. Encoding is a one-way door

The scariest incident we had involved no SQL at all. A Shift-JIS (CP932) source file was edited by a tool that assumed UTF-8. Every Japanese character became U+FFFD - and once saved, the original bytes were gone. No conversion recovers that; only a pre-damage backup does.

If your legacy estate includes Windows codebases, you very likely have non-UTF-8 files (CP1252 counts). Two defenses:

  1. A PreToolUse hook that blocks edits on any file that doesn't decode as strict UTF-8, and on any file already containing U+FFFD (previously damaged - editing entrenches it).
  2. A day-one census: list every source file that fails strict UTF-8 decoding, and decide deliberately, per file, whether to convert or keep the legacy encoding.

3. "Dead code" isn't dead, and frozen paths beat trust

In a codebase with no tests and no original author, the phrase "this looks unused" is a trap. Our rule: paths nobody currently understands go into a frozen-paths.txt. A hook lets Claude read and study them freely, but any edit requires human approval. As investigation turns unknowns into knowns, paths get unfrozen.

This inverts the usual dynamic. Instead of hoping the AI is careful everywhere, you declare exactly where carefulness is mandatory, and the machine enforces it.

4. The first deliverable is a map, not a diff

The most valuable Claude Code sessions on a legacy system produce zero code changes. We run a phased, read-only investigation:

  1. Perimeter: inventory, entry points (URLs, mains, cron), and exits (DB connections, file writes, network calls)
  2. Data map: which tables does the code actually read/write, from which files - a table-to-code cross reference
  3. Money paths: end-to-end traces of the 2-3 flows that matter, verified against a real record
  4. Risk register: what's still not understood (stays frozen), single points of failure, time bombs

Every discovery goes into a NOTES.md. A session that only added knowledge is a successful session - and after a few weeks, the undocumented system has documentation again, written as a by-product.

5. Legacy landmines your modern-stack instincts will miss

  • Y2K38 is today's bug: 32-bit PHP overflows epoch timestamps past 2038-01-19. Certificate expiries and long-term date math hit this now. Route future dates through DateTime, never epoch integers.
  • Exit code 0 lies: legacy batch scripts swallow errors. Verify the artifact (file exists, is fresh, row count moved), not the exit code - especially for unattended jobs.
  • Session-dependent services: a process started at login sees mapped network drives; the same process as a Windows service does not. Changing how something starts silently changes what it can see. Enumerate those dependencies before touching startup behavior.
  • Byte-defined columns: VARCHAR2(10) in bytes truncates multibyte text at 10 bytes, not 10 characters. Validate in bytes.

Wrapping up

None of this requires anything exotic - hooks, a text file of frozen paths, and discipline about read-only investigation. If you'd rather not build it from scratch, I packaged our hooks, CLAUDE.md templates and playbooks into two small kits: a team governance pack and a legacy survival kit. But the ideas above are the valuable part, and an afternoon is enough to implement them yourself.

Questions about running Claude Code against genuinely old systems welcome - this is my day job.

Top comments (0)