Dismantling 300 `any`-typed codebases step by step with Codex CLI and AGENTS.md
If your team has been putting off a TypeScript strict migration, you've probably been through this at least once. The day you first run tsc --strict --noEmit, the terminal fills with red and the error count scrolls off the screen. I remember closing the terminal window on the first day of a 200,000-line Node.js + React monorepo migration a couple of years ago. I couldn't find a starting point.
This post is about the way out I found from that situation. By combining Codex CLI's codex exec command with AGENTS.md, you can delegate the repetitive work of replacing hundreds of anys with semantically correct types to an AI agent. The key is letting go of the urge to fix everything at once, and making the agent follow a phased commit strategy that enables compiler flags one at a time.
Two premises to establish upfront. Codex CLI is a terminal-based coding agent that OpenAI open-sourced in April 2025. AGENTS.md is a proposed project context file convention that multiple AI coding tools can reference in common. The governance body behind standardization is not clearly identified in public materials, so this post treats it only from the practical angle of "a convention that multiple tools can reference."
Why This Combination Is Needed Now
The Changes TypeScript 6/7 Foreshadow
As of September 2026, TypeScript 6.0 has not yet been officially released. Microsoft has publicly announced a native compiler rewritten in Go (codenamed Corsa), targeting TypeScript 7.0, with improved build performance as the primary goal. Specific multipliers (e.g., N times faster) have not been finalized in official benchmarks, so this post only treats "a native compiler transition is planned" as fact.
The claim that "strict will become the default in 6.0" is community speculation, not something explicitly stated in the official roadmap. However, there is one thing that is practically certain. Running tsc --init on a new project has already generated a tsconfig.json with strict: true enabled by default for several years. The standard diverges between new and legacy projects, and closing that gap is advantageous from the perspective of hiring, library compatibility, and future upgrades.
In other words, the situation has shifted from "we'll get to it someday" to "every time a new hire joins, the friction comes back, and it gets harder to keep up with third-party type updates."
Why Manual Work Stalls on Large Codebases
According to the Airbnb Engineering Blog, the team had to build a separate automation tool called ts-migrate to move a large JavaScript codebase to TypeScript. Converting files one by one manually burns engineer time on repetitive editing rather than type improvement.
The practical conclusion here is one: deterministic transformations should go to codemods (e.g., jscodeshift), and contextual judgment to AI agents — that's how you stay within a manageable review burden. There are no validated statistics on how much each tool handles, but the division of labor itself is a practical pattern shared across many migration case studies.
One point worth noting: the codemod mention in this post is an architectural explanation about "the need for a deterministic transformation layer." Which codemod to use depends on the project. For React props renaming, a jscodeshift-based script is common; for import cleanup, a ts-morph script is a frequent choice.
AGENTS.md — The Single Source of Truth for Your Agent
The core value of AGENTS.md is simple. Team members and AI agents reference the same conventions from the same file. Codex CLI officially reads this file and uses it as context. Support in other tools (Claude Code, Cursor, etc.) varies by tool and version, so it's safer to check each tool's latest documentation before adopting it. Tool-specific files exist separately — Claude Code's CLAUDE.md, Cursor's .cursorrules — and it's better not to assume any mutual fallback behavior between them and AGENTS.md.
Minimal Example (Recommended)
First, a short version that's actually maintainable. This length keeps review and update burden low.
# AGENTS.md
## Context
Node.js + React monorepo, ~200k lines. TypeScript strict migration Phase 2.
## Type Rules
- No `any`. If unavoidable, use `unknown` with narrowing.
- Type guards instead of `as` casting.
- Explicitly annotate return types in function signatures.
## Validation
- After changes, `tsc --noEmit` must exit with code 0.
- `pnpm test --changed` must pass.
## Commits
- One PR per module. Message: `feat(types): remove any from <module>`.When You Need to Expand
It's natural for the file to grow as conventions accumulate, but don't forget it continuously occupies the context window. There's no validated threshold number, but empirically, keeping it scannable within one screen (~100 lines) helped the agent not miss rules. Sections that tend to grow — project-specific exceptions, domain glossaries — are better split into separate documents and referenced from AGENTS.md as links.
Phased Commit Strategy — Why You Can't Enable Everything at Once
Adding strict: true to the root tsconfig.json takes one second. But immediately after, hundreds to thousands of errors erupt simultaneously. In that state, neither the agent nor a human can tell where to begin.
The recommended order is as follows.
After each phase, confirm that tsc --noEmit returns exit code 0 before committing. For reviewers, the context is clear — "this PR only removes noImplicitAny errors" — which dramatically reduces review time.
Per-Phase tsconfig.json Settings
Phase 1 — Coexist with JS files:
The goal of this phase is not to catch type errors. It's to bring only files renamed to .ts into the compilation scope, leaving JS files in place without deleting them. With checkJs: false, JS files still receive no checks, so the team can start per-file migration without stopping feature development.
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"target": "ES2022",
"module": "NodeNext"
}
}Phase 2 — Block implicit any:
From this point, parameters without type annotations in .ts files produce errors. Code with explicit : any still passes.
{
"compilerOptions": {
"allowJs": true,
"noImplicitAny": true
}
}Phase 3 — Add null safety:
This phase exposes a large volume of undefined | null-related errors. This is where optional chaining (?.) and nullish coalescing (??) get applied at scale.
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true
}
}Final Phase — Full strict:
{
"compilerOptions": {
"strict": true
}
}Building an Automation Loop with Codex CLI
Basic Execution Pattern
git checkout -b migrate/remove-implicit-any
codex exec "Fix noImplicitAny errors in TypeScript files under the src/services/ directory.
Constraints:
- No as casting or explicit any usage
- Start from leaf modules (files with no dependencies) and proceed in dependency order
- After modifying each file, run tsc --noEmit and confirm exit code 0
- Summarize in one line what was changed per modified file"Since codex exec modifies actual files, it's safest to run it on a clean working tree, on a feature branch. Codex CLI offers approval mode and sandbox policy options at runtime, so for a first attempt, enabling approval mode to require human confirmation before file writes is recommended. Specific flag names and defaults change between versions, so check current options with codex exec --help.
The Validation Loop the Agent Runs
The feedback loop the agent performs autonomously looks like this.
The important thing here is not stopping at compile success — including test execution in the loop. The reason is covered below in the failure cases.
Handling Libraries Without Third-Party Types
When the agent encounters a library with no @types/* package or bundled .d.ts, it stalls too. For these cases, declaring a policy in AGENTS.md or placing ambient declarations in a dedicated types/ directory is practical. Here's a conceptual example (not a real package).
// types/legacy-analytics.d.ts (conceptual example)
declare module 'legacy-analytics-sdk' {
export interface TrackOptions {
event: string;
properties?: Record<string, unknown>;
}
export function track(options: TrackOptions): void;
}Tracking Remaining any with ESLint
Even after Codex has done its work, some any may have slipped through. typescript-eslint rules can catch them in CI. As of 2026, ESLint v9+ is common, so the example uses flat config format.
// eslint.config.js
import tseslint from 'typescript-eslint';
export default tseslint.config(
...tseslint.configs.recommendedTypeChecked,
{
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unsafe-assignment': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
},
},
);Projects still on ESLint v8 can add the same rules to their existing .eslintrc.json.
Per-File Incremental Migration — @ts-nocheck and Community Tools
If the whole team needs to keep shipping features while migrating, per-file incremental migration is the realistic choice. The directive officially recognized by the TypeScript compiler is // @ts-nocheck, which skips type checking for the entire file.
// @ts-strict-ignore, commonly mentioned in the Angular community, is not a native TypeScript directive — it requires a separate tool (e.g., ts-strict-plugin) to function as a marker. Adopting it requires installing that plugin and managing per-file strict rules. Without that setup, the comment is just an ordinary comment that gets ignored.
A simple approach is to start with @ts-nocheck, then gate on the remaining file count in CI.
IGNORE_COUNT=$(grep -rl "@ts-nocheck" src/ | wc -l)
echo "Unmigrated files: $IGNORE_COUNT"
if [ "$IGNORE_COUNT" -gt 50 ]; then
echo "Migration pace is below target"
exit 1
fiTrade-offs — An Honest Assessment
| Item | Pros | Considerations |
|---|---|---|
| Agent automation | Can delegate repetitive type inference and modification work | AI may infer semantically incorrect types. Test execution must be included in the loop |
| Phased commit strategy | Minimizes review cost with per-module PRs, allows parallel feature development | Need to manage merge conflicts between migration branches and main branch |
| AGENTS.md | Team members and agent reference the same conventions | Context cost grows as the file gets larger. Keep only the essentials |
tsc --noEmit loop |
Clear completion criteria, easy CI integration | Full check time can grow long on large codebases |
@ts-nocheck file marking |
Whole-team participation structure, incremental migration possible | Markers left in place become technical debt |
Moments When the Agent Fails Silently — A Real Example
The biggest trap with this approach is the agent loosening types to get past the compiler. Here's a condensed version of a real pattern I encountered during migration.
Before (implicit any):
export function mergeConfig(base, override) {
return { ...base, ...override };
}After as produced by the agent (compiles, but types are meaningless):
export function mergeConfig(base: unknown, override: unknown): unknown {
return { ...(base as object), ...(override as object) };
}This code passes noImplicitAny. But the moment call sites try to access properties of the return value, errors erupt again — or new as casts appear. It directly contradicts the "no as" rule in AGENTS.md, but when the agent is in a hurry, it takes this shortcut. What was actually wanted is this:
export function mergeConfig<T extends object, U extends object>(
base: T,
override: U,
): T & U {
return { ...base, ...override };
}To catch these cases, passing tsc alone isn't enough — test execution covering call sites and no-unsafe-* ESLint rules both need to be inside the loop. In particular, the pattern of the agent escaping to unknown in complex generics, callback chains, and event handler signatures should be prioritized during review.
Closing — What to Do Next
If you want to actually adopt the workflow in this post, here's the order I'd start with.
- Run
grep -rn ": any" src/ | wc -landtsc --strict --noEmit 2>&1 | wc -lon your current codebase and record the starting numbers. These declining figures are your only progress indicator. - Write AGENTS.md short — at the minimal example level above — and get one round of team review. Rules a human can't understand, an agent won't follow either.
- Pick one utility directory with the fewest dependencies and run
codex execin approval mode. Sharing the resulting diff in the team channel makes the next discussion much easier.
Handing the entire migration to an agent, or having humans do it all manually — neither is the optimal solution. Placing deterministic validators — compiler, tests, linter — in the middle, with the agent handling repetitive work and humans focusing on ambiguous type decisions and reviewing failure cases, feels like the most manageable approach right now.
References
- OpenAI Codex CLI Official Documentation
- AGENTS.md Official Site
- TypeScript Native Port (Corsa) Announcement
- TypeScript Roadmap
- ts-migrate: A tool for migrating to TypeScript at scale (Airbnb Engineering)
- typescript-eslint Official Documentation
- ESLint Flat Config Documentation
- jscodeshift
- ts-morph
- eslint-plugin-typescript-strict-plugin (Allegro)