Codemods skill for Codex
Change 500 files with code you can rerun. Codex looks for an official migration codemod first, then picks the lightest syntax-aware tool that can express the change: ast-grep for call shapes, jscodeshift or ts-morph when scope or types matter, LibCST for Python, OpenRewrite for Java. Transforms get fixture tests for aliasing and shadowing, the diff is sampled before applying, a second run must change nothing, and leftovers are listed for humans.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- codemods Installs to ~/.agents/skills/codemods/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install codemods The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project codemods Installs to .agents/skills/codemods/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/codemods.zipunzip -o codemods.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) codemods PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $codemods, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Performs large mechanical code changes with syntax-aware tools instead of hand edits or blind regex - ast-grep rules, jscodeshift and ts-morph transforms for JavaScript and TypeScript, comby templates, LibCST for Python, OpenRewrite recipes for Java and Kotlin, and official framework codemods - with fixture tests, dry runs, idempotency checks and a leftover report. Use when renaming an API across hundreds of files, migrating a library or framework version, replacing a deprecated call pattern, or when asked to write or run a codemod.
Use it when
- The same change must be made in dozens or hundreds of places
- Migrating to a new library or framework major version
- Replacing a deprecated API pattern across a monorepo
- A previous regex find-and-replace broke code
Use something else when
- The change is a behaviour-preserving restructure of a few modules: use safe-refactoring
- You only need to find code by pattern, not rewrite it: use regex-crafting
- You are upgrading dependency versions and reading changelogs: use dependency-upgrades
How to check the result
Fixture tests pass, types and the test suite are green, a second codemod run produces zero changes, and a search for the old API returns only the listed manual leftovers.
The mistake it prevents
Using regex for a code-shaped change, which edits matches inside comments and strings, misses calls split across lines, and renames shadowing local variables.
A codemod is done when the transform is code you could run again (and running it again changes nothing), it has before/after fixture tests including the awkward cases, the diff was sampled by a human, types and tests pass, and every occurrence it could not handle is listed for manual follow-up. The transform and its output land as separate, reviewable units.
Workflow
- [ ] 1. Read AGENTS.md; check for an official codemod for this migration
- [ ] 2. Inventory occurrences and classify them
- [ ] 3. Pick the tool (table below)
- [ ] 4. Write the transform with fixture tests (happy path + edge cases)
- [ ] 5. Dry run on the repo; sample the diff
- [ ] 6. Apply; run the formatter; run types and tests
- [ ] 7. Re-run the codemod: expect zero changes (idempotent)
- [ ] 8. Report leftovers; split the change for review if large
1. Use the official codemod first
Framework and library authors often ship one: npx @next/codemod@latest <transform> <path>,
the codemod.com registry (npx codemod <name>), Angular ng update migrations, OpenRewrite
recipes for Spring Boot and JUnit, pyupgrade-style fixes through ruff check --fix --select UP,
cargo fix --edition, go fix. Running these needs network to fetch the package; ask in
an offline sandbox. Read the codemod’s README for what it does not cover.
2. Inventory
rg -c "from ['\"]old-lib['\"]" --type ts | wc -l # files importing it
ast-grep run -p 'oldFn($$$ARGS)' -l ts src --json=stream | wc -l # syntax-aware match count
Classify occurrences into: mechanical (same shape every time), parameterised (shape varies but rule is clear), and judgement (needs a human). The codemod covers the first two; the third becomes a TODO list. If judgement cases exceed about 20%, reconsider whether a codemod is the right tool.
Tool choice
| Change | Tool | Why |
|---|---|---|
| Literal string with no ambiguity (a URL, a config key) | rg -l + sed -i or perl -pi | Simplest; check every hit |
| Call or expression shape, any common language | ast-grep | Tree-sitter patterns with metavariables, YAML rules, built-in tests |
| Lightweight structural match in languages without a good parser setup | comby | Balanced-delimiter templates, language-agnostic |
| JS/TS needing scope, imports, JSX awareness | jscodeshift | Mature AST API, preserves formatting via recast |
| TS needing type information (which overload, which class) | ts-morph | Full TypeScript compiler API |
| Python | LibCST codemods | Lossless concrete syntax tree, keeps comments and formatting |
| Java, Kotlin, Maven/Gradle builds | OpenRewrite | Type-attributed recipes, large recipe catalog |
| PHP | Rector | Same idea for PHP |
| Go | gofmt -r 'a -> b', gopls rename, go fix | Built-in tooling understands Go semantics |
Default to ast-grep unless the change needs scope or type information.
ast-grep
One-off rewrite (prints a diff; --update-all applies):
ast-grep run -l ts -p 'console.log($$$ARGS)' -r 'logger.debug($$$ARGS)' src/
ast-grep run -l ts -p 'console.log($$$ARGS)' -r 'logger.debug($$$ARGS)' src/ --update-all
Reusable rule with constraints:
# codemods/moment-format.yml
id: moment-format-to-date-fns
language: TypeScript
rule:
pattern: moment($DATE).format($FMT)
constraints:
FMT:
kind: string
fix: format($DATE, $FMT)
ast-grep scan -r codemods/moment-format.yml src/ # report matches
ast-grep scan -r codemods/moment-format.yml src/ --update-all
$X matches one node, $$$X matches zero or more. Use the ast-grep binary name; sg
collides with a system command on some Linux distributions. Add rule tests with
ast-grep test (valid/invalid snippets in a test YAML) when the rule will be reused.
jscodeshift
// codemods/rename-import.ts
import type { API, FileInfo } from "jscodeshift";
export const parser = "tsx";
export default function transform(file: FileInfo, api: API) {
const j = api.jscodeshift;
const root = j(file.source);
let changed = false;
root.find(j.ImportDeclaration, { source: { value: "old-lib" } }).forEach((path) => {
path.node.source.value = "new-lib";
for (const spec of path.node.specifiers ?? []) {
if (spec.type === "ImportSpecifier" && spec.imported.name === "createClient") {
// Keep the local binding: `import { makeClient as createClient }`.
// Call sites stay valid and shadowed locals cannot be hit by accident.
spec.imported = j.identifier("makeClient");
spec.local = j.identifier(spec.local?.name ?? "createClient");
}
}
changed = true;
});
return changed ? root.toSource() : undefined; // undefined = file skipped, untouched
}
npx jscodeshift -t codemods/rename-import.ts src --extensions=ts,tsx --dry --print | head -80
npx jscodeshift -t codemods/rename-import.ts src --extensions=ts,tsx
Renaming the call sites as well needs reference tracking: do it as a second step with
ts-morph (identifier.rename("makeClient") follows references through the language
service) rather than renaming every identifier with that name.
Test with fixtures: __testfixtures__/name.input.ts and name.output.ts plus
defineTest from jscodeshift/src/testUtils, or a plain test that calls the transform
on strings. Include: aliased imports, a local variable that shadows the name, re-exports
(export { createClient } from "old-lib"), type-only imports, and JSX.
comby, LibCST, OpenRewrite
comby 'assertEquals(:[expected], :[actual])' 'assertThat(:[actual]).isEqualTo(:[expected])' .java -d src/test # prints diff
comby 'assertEquals(:[expected], :[actual])' 'assertThat(:[actual]).isEqualTo(:[expected])' .java -d src/test -i
python -m libcst.tool initialize . # once: creates .libcst.codemod.yaml
python -m libcst.tool codemod mymods.RenameCall src/ # runs a CodemodCommand subclass
mvn -U org.openrewrite.maven:rewrite-maven-plugin:dryRun \
-Drewrite.recipeArtifactCoordinates=<recipe-artifact>:RELEASE \
-Drewrite.activeRecipes=<fully.qualified.RecipeName>
# review target/rewrite/rewrite.patch, then run the same with :run
Look up recipe names in the OpenRewrite recipe catalog rather than guessing them.
Apply and verify
<run the codemod>
npx prettier --write $(git diff --name-only) # or ruff format / gofmt / the repo's formatter
git diff --stat | tail -3
<typecheck> && <tests> # commands from AGENTS.md
<run the codemod again>; git diff --quiet && echo "idempotent" # after committing the first run
rg -n "old-lib|createClient" src/ # leftovers
Sampling: read at least 20 changed hunks chosen across different directories
(git diff --name-only | sort -R | head -20), plus every file with an unusually large hunk.
Gotchas
- Regex across multi-line calls, comments and strings: it matches inside comments and misses calls split over lines. Use a syntax-aware tool for anything code-shaped.
- Shadowing: a local variable or parameter with the same name as the import must not be renamed. Scope-aware tools (jscodeshift scopes, ts-morph, LibCST metadata) handle this; ast-grep patterns do not by themselves.
- Indirect references: dynamic property access, string-based registries, dependency
injection tokens, mocks in tests (
vi.mock("old-lib")), docs and config files. Search for the string form after the codemod. - Generated files (protobuf, GraphQL codegen, OpenAPI clients): change the generator input and regenerate; never codemod generated output.
- In-flight branches will conflict. Announce the change, land it quickly, and keep the codemod in the repo (or the PR description) so others can run it on their branches.
- A green type check proves syntax and types, not behaviour. Run the tests, and for semantic changes (argument order, defaults) add a test that would catch a swap.
Output
## Codemod: old-lib createClient -> new-lib makeClient
- Tool: jscodeshift (scope-aware rename needed); transform at codemods/rename-import.ts
- Fixtures: 7 cases incl. aliased import, shadowed local, re-export, JSX
- Result: 214 files changed, idempotent (second run: 0 changes)
- Checks: tsc clean, 1,982 tests pass, prettier applied
- Leftovers (manual): 3 dynamic requires in scripts/legacy/*.js, 1 string reference in docs/setup.md
- Review plan: transform commit + generated commit; split by package per CODEOWNERS What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Codemods"
short_description: "Change 500 files with code you can rerun"
default_prompt: "$codemods Write a codemod that replaces moment().format() calls with date-fns across the repo" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.