This week I ran the same codemod, at the same version, across four of my repos. It left a different gap in each one — and in two repos, its raw output didn't even type-check.
I committed that output as-is anyway. On purpose. Here's why.
The setup
The codemod is nene2-a1-hooks-to-model, from my published standards package (@hideyukimori/nene2-standards@1.1.0). It does one boring thing: move React hooks from features/*/hooks/ into model/, per the layering convention my frontends share, and rewrite the imports that point at them.
I ran it, one-shot via npx, in four repos: nene-field, nene-suite, nene-profile, and nene-contact.
Four repos, four different gaps
Same tool. Same version. Four outcomes:
- nene-field (PR #102): the clean case. 15 hooks moved across 10 slices, 15 files' imports rewritten, +18/−18 lines, every move detected as a 100%-similarity rename. Nothing to add by hand.
- nene-suite (PR #389): the moves were fine (18 files, all renames at 100% similarity), but the codemod reprinted two files it touched with formatting that disagreed with the repo's Prettier config. A +2/−2 formatting fight.
-
nene-profile (PR #120):
hooks/contained a non-hook file — a shared zod schema. The codemod moved the two hooks that imported it, left the schema behind, and didn't rewrite their relative./preset-schemaimports. Raw output:tsc -bred, TS2307 × 2. - nene-contact (PR #401): 13 hooks and a co-located test moved — and zero imports rewritten. 24 importer references now pointed at files that weren't there. Raw output alone: type errors, by design of the PR.
So a mechanical migration needed a human (well, a human-supervised agent) to finish it in three of four repos. That's normal. The question is what you do about it.
The temptation is to patch silently
The easy move: fix the imports, run Prettier, squash, open a green PR titled "applied the codemod."
Now the diff is a lie. It claims a machine did all of it. Your reviewer — future you, a colleague, or an AI agent reading history to learn "how migrations are done here" — can no longer tell which lines a deterministic tool produced and which lines a tired human typed at 1 a.m.
Commit 1 is what the machine did. Commit 2 is what it couldn't. Never mix them.
The pattern: commit-separated gaps, with rename proof
Every one of these PRs follows the same discipline:
Commit 1: the codemod's raw output, byte-for-byte, zero edits. Even if it's red. In nene-contact, commit 3b71717 is intentionally broken on its own — the PR body says so out loud, so nobody bisecting later is surprised.
Commit 2 (and 3): mechanical completion only, one kind of mechanical thing per commit. In nene-profile that meant two commits: b58aaa9 is a pure git mv of the leftover schema file, and e0e0e40 is a single import-path line. In nene-suite, commit 8044f11 is only prettier --write restoring the repo's formatting on the two reprinted files. In nene-contact, commit 661f2b0 fixes the 24 importer references and re-runs the formatter.
And you paste the proof in the PR body. For the git mv commit, that's rename detection:
$ git show b58aaa9 --find-renames --name-status --format=
R100 frontend/src/features/mapping-presets/hooks/preset-schema.ts frontend/src/features/mapping-presets/model/preset-schema.ts
1 file changed, 0 insertions(+), 0 deletions(-)
R100 — 100% similarity, zero insertions, zero deletions. The reviewer doesn't have to trust my adjective "mechanical." Git certifies it.
For an import fix, the proof is exhaustiveness: in nene-profile I noted that grep -rn "hooks/" frontend/src returned exactly the one line commit 3 changes. The completion commit isn't "some cleanup" — it's provably the whole gap and nothing else.
What this buys you
Review time collapses to where it belongs. Commit 1 is audited as "do I trust this tool at this version" — once, cheaply, across every repo it runs in. Commits 2–3 are audited line-by-line, but they're tiny (1 line; +2/−2; 24 one-pattern references) and each does one nameable operation.
And the gap itself becomes a first-class artifact. Because the profile run isolated exactly what the codemod failed to do, filing it upstream was trivial: nene2-fleet-tooling#83 documents the leftover-file and unrewritten-import behavior, with the TS2307 reproduction pasted in. That issue is still open — the tool isn't fixed. But the workaround is a documented, repeatable pattern instead of four divergent hand-patches, and when the fix lands, commit 2's exact shape tells us what should disappear from future runs.
Silent hand-patching produces none of this. It just makes the tool look better than it is.
Where I'd skip all this
Honestly: a solo throwaway repo, a one-file move, no reviewers, no agents reading history? Just fix it and squash. The ceremony has a cost.
The discipline earns its keep the moment anyone — human or model — needs to answer "what did the machine actually do?" from the record. In my case that's every migration, because the record is what my AI agents learn the house style from.
Takeaways
- A codemod PR should let a reviewer audit "machine work" and "human completion" separately — that's a commit boundary, not a PR description.
- Prove the mechanical-ness:
git show --find-renamesfor moves (R100), a grep count for import fixes, "formatter only" for reprints. - When the tool leaves a gap, file it upstream with the isolated diff — the separation gives you the reproduction for free.
Primary sources
- nene-profile PR #120 (commits
9c28709/b58aaa9/e0e0e40, rename proof in body) and issue #119 - nene-suite PR #389 (commits
e28f0b0/8044f11) - nene-contact PR #401 (merge
59f283e, commits3b71717/661f2b0) - nene-field PR #102 (move-only, +18/−18)
- Upstream gap: nene2-fleet-tooling#83 (open)
Related: I built a tiny PHP framework for AI-readable business APIs
How do you make codemod PRs reviewable — squash and trust the tool, or separate and prove it? I'd genuinely like to hear where you draw the line.
── Hideyuki Mori (Ayane International) 🔗 hideyuki-mori.com
Top comments (0)