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.

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- codemods

Installs to ~/.agents/skills/codemods/ for every project on this machine.

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.

~/.agents/skills/codemods/SKILL.md

--- name: codemods description: Performs large mechanical code changes with syntax-aware tools instead of hand edits or blind regex - ast-grep rules, jscodeshift and ts-morph… license: MIT ---

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

ChangeToolWhy
Literal string with no ambiguity (a URL, a config key)rg -l + sed -i or perl -piSimplest; check every hit
Call or expression shape, any common languageast-grepTree-sitter patterns with metavariables, YAML rules, built-in tests
Lightweight structural match in languages without a good parser setupcombyBalanced-delimiter templates, language-agnostic
JS/TS needing scope, imports, JSX awarenessjscodeshiftMature AST API, preserves formatting via recast
TS needing type information (which overload, which class)ts-morphFull TypeScript compiler API
PythonLibCST codemodsLossless concrete syntax tree, keeps comments and formatting
Java, Kotlin, Maven/Gradle buildsOpenRewriteType-attributed recipes, large recipe catalog
PHPRectorSame idea for PHP
Gogofmt -r 'a -> b', gopls rename, go fixBuilt-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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem