Building a safety net with AGENTS.md and a diff loop when migrating callback code to async/await using Codex CLI
When you inherit a legacy Node.js project, the first thing you encounter is callback hell. Callbacks inside callbacks, nested deeper and deeper. Error handling is inconsistent at every level, and stack traces give you no clue where things actually broke. As of 2026, async/await remains the most widely used async pattern in new Node.js code, but Promise chains and stream-based processing are still valid tools. That said, callback-heavy code carries a heavy maintenance burden, and fixing it by hand one function at a time is grueling work. With Codex CLI, you can automate this process — and the key is using AGENTS.md to clearly define the boundaries the agent is allowed to touch.
To share one lesson from my own trial and error: simply telling Codex "convert these files to async/await" makes it touch too much. It adds new dependencies to package.json, modifies test files you never intended to change, and simplifies async logic that should run in parallel into a sequential await chain. I once ran it in Full Auto mode and came back to find the entire test/ directory had been rewritten — I still remember the cold sweat. Designing constraint boundaries with AGENTS.md, then iterating through diff and review steps to validate each change, is what solves this problem.
This article walks through the actual process of converting legacy callback code with Codex CLI, covering how to write AGENTS.md and structure the diff review loop in concrete detail. It also addresses what util.promisify covers and what it doesn't. All first-person anecdotes that follow are written in the first person.
Why This Conversion Matters Now
The Real Cost of Callback Hell
The difference just three levels of nesting makes is immediately visible. The example below is conceptual code that doesn't assume any specific DB library.
// Classic callback hell (conceptual example)
function getUserData(userId, callback) {
db.getUserById(userId, (err, user) => {
if (err) return callback(err);
fs.readFile(`./profiles/${user.id}.json`, 'utf8', (err, profileData) => {
if (err) return callback(err);
cache.set(`user:${userId}`, profileData, (err) => {
if (err) return callback(err);
callback(null, { user, profile: JSON.parse(profileData) });
});
});
});
}// Converted to async/await (conceptual example)
async function getUserData(userId) {
const user = await db.getUserById(userId);
const profileData = await fs.promises.readFile(`./profiles/${user.id}.json`, 'utf8');
await cache.set(`user:${userId}`, profileData);
return { user, profile: JSON.parse(profileData) };
}Note that with real libraries like mysql2 or pg, query() typically returns a rows array, so you need to destructure with const [rows] = await db.query(...). The example above is simplified to illustrate the flow.
The logic is readable at a glance, and a single try/catch catches all errors. Stack traces are output in a meaningful form, making root cause analysis much faster.
util.promisify as the Starting Point for Automation
util.promisify(), introduced in Node.js 8, wraps functions that follow the standard error-first callback pattern — where the last argument is a (err, result) callback — into Promise-returning functions.
const util = require('util');
const fs = require('fs');
// Before util.promisify
fs.readFile('./data.json', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
// After util.promisify
const readFile = util.promisify(fs.readFile);
async function loadData() {
const data = await readFile('./data.json', 'utf8');
console.log(data);
}However, util.promisify() only supports the standard pattern where the callback is the last argument and the first callback argument is an error object. Functions with non-standard signatures require separate handling via the util.promisify.custom symbol. Making this boundary explicit for Codex is one of AGENTS.md's core responsibilities.
Choosing a Conversion Strategy: Codex Alone or Combined with codemod
For large codebases, a hybrid strategy combining an AST-based codemod tool like jscodeshift with Codex can be effective. The idea is to use codemod first for mechanical transformations with clear patterns, then use Codex CLI for parts that require semantic judgment — such as whether to parallelize or how to change error propagation. There are reports that transformations with uniform shapes, like migrating Mocha test done callbacks to async functions, are better handled by codemod (see References).
Conversely, if callback signatures vary per function or the parallel/sequential decision differs case by case, it's better to handle everything with Codex from the start. Survey the project first, judge whether the patterns are uniform or not, then decide on your strategy.
Designing AGENTS.md Constraints
How to Communicate Boundaries to the Agent
AGENTS.md is a Markdown file placed at the repository root. Codex CLI reads it to understand the project structure, commands, and what it should "always do / ask first / never do." It's more of a convention adopted by Codex CLI than an officially standardized spec, and it's safer to check each tool's official documentation to confirm support from other tools.
An AGENTS.md tailored for callback conversion work might look like this:
# Project Context
Legacy Node.js 16 e-commerce backend. Currently migrating callback-based async code to async/await.
## Conversion Scope
### Always
- Modify only files under src/legacy/**
- Wrap error-first callbacks with util.promisify or convert directly to async functions
- Confirm `npm test` passes after conversion
- Update JSDoc return types of converted functions to Promise<T>
### Ask First
- When a non-standard callback signature is found where util.promisify applicability is unclear
- When a callback pattern appears to have parallel execution intent (confirm whether to convert to Promise.all)
- When callers of the function being converted exist outside src/legacy/
### Never
- Directly modify the test/ directory
- Add or modify dependencies in package.json
- Modify files outside src/legacy/
- Convert patterns that explicitly ignored callback errors into empty catch blocks
## Non-Standard Callbacks (require manual handling)
- src/legacy/queue/processor.js — onComplete(result, err) has non-standard argument order
- src/legacy/vendor/oldLib.js — callback is the second argument
## Commands
- Test: npm test
- Lint: npm run lintIf the file grows too large, it risks being truncated from the context window. In practice, it's safer to summarize each item in one or two lines and move detailed rules to separate documents.
Approval Policy and Sandbox Settings
The approval and sandbox options provided by Codex CLI are broadly categorized as follows. For exact key names and configuration locations, consult the official settings documentation for your version of Codex CLI.
| Setting | Meaning | When to Use |
|---|---|---|
| Suggest mode | Suggests changes only; all applications require approval | Early in migration, when uncertainty is high |
| Auto Edit mode | File edits are automatic; command execution requires approval | After conversion patterns are validated |
| Full Auto mode | All operations run automatically | Only after AGENTS.md constraints are thoroughly validated |
| Workspace write sandbox | Allows workspace writes, blocks external access | General code conversion work |
| Full access sandbox | Allows all access | Rarely used |
The recommended starting combination is a per-request approval policy plus workspace write sandbox. I recommend a staged approach: start in Suggest mode, build trust by reviewing conversion patterns visually, then switch to Auto Edit mode.
The Diff Review Loop in Practice
Slash commands used in a Codex CLI interactive session (e.g., /diff, /review) are entered within the session. The notation below means typing them directly at the interactive prompt; commands run in the shell are distinguished separately with the codex ... format. Check the exact command names and usage with /help in your version of Codex CLI.
Step-by-Step Command Flow
Step 1: Run the conversion command (shell)
codex "Convert all callback functions in the src/legacy/auth/ directory to async/await. \
Apply util.promisify where possible, and rewrite directly where it's not."Codex presents the list of affected files and a conversion plan first. In Suggest mode, you can approve each file change individually.
Step 2: Confirm scope with diff (inside interactive session)
/diffIf the test/ directory or package.json — designated as Never in AGENTS.md — appear in the list, stop immediately. Validating scope first is essential.
Step 3: Confirm quality with review (inside interactive session)
/review/review outputs a report of findings without modifying any code. Each round's results are saved as independent transcript turns, so you can view previous and current rounds side by side to track whether issues have been resolved. Common findings in the context of callback conversion include:
- Callbacks that ran in parallel converted into a sequential
awaitchain - Patterns that ignored errors in callbacks turned into Promise rejections with no catch
- Non-standard callback signatures passed directly to
util.promisify
Step 4: Re-validate after edits
# Re-confirm scope with /diff in the interactive session, then switch to shell
npm testRepeat this loop until you're ready to commit. npm test must pass before moving to commit.
Parallel Processing Patterns — The Most Common Pitfall in Automatic Conversion
Honestly, this is the part that demands the most attention. When logic that ran in parallel in callback code is converted into a simple await chain, performance degrades.
// Original: intentional parallel execution with callbacks
function fetchUserAndOrders(userId, callback) {
let user, orders, done = 0;
db.getUser(userId, (err, u) => {
if (err) return callback(err);
user = u;
if (++done === 2) callback(null, { user, orders });
});
db.getOrders(userId, (err, o) => {
if (err) return callback(err);
orders = o;
if (++done === 2) callback(null, { user, orders });
});
}// Incorrect conversion: serialization causes unintended performance degradation
async function fetchUserAndOrders(userId) {
const user = await db.getUser(userId); // runs sequentially
const orders = await db.getOrders(userId); // doesn't start until user is done
return { user, orders };
}
// Correct conversion: preserves parallel execution
async function fetchUserAndOrders(userId) {
const [user, orders] = await Promise.all([
db.getUser(userId),
db.getOrders(userId),
]);
return { user, orders };
}If you specify "confirm when a pattern with apparent parallel execution intent is found" in the Ask First section of AGENTS.md, Codex will stop and ask rather than automatically converting when it encounters these patterns.
Non-Standard Callbacks and util.promisify.custom
Functions that don't follow the standard pattern must be manually defined with the util.promisify.custom symbol. Listing these in AGENTS.md in advance is critical.
const util = require('util');
// Non-standard: callback order is (result, err)
function legacyOp(input, callback) {
// callback(result, err) — non-standard order
}
// Define manually with util.promisify.custom
legacyOp[util.promisify.custom] = function(input) {
return new Promise((resolve, reject) => {
legacyOp(input, (result, err) => {
if (err) reject(err);
else resolve(result);
});
});
};
// Now promisify can be applied
const legacyOpAsync = util.promisify(legacyOp);If these functions aren't listed in the non-standard section of AGENTS.md, the agent may try to apply standard util.promisify directly and produce incorrect error-handling code.
Tradeoffs and Practical Considerations
| Item | Details |
|---|---|
| Improved readability | Nested callback structures flatten into sequential code, making logic flow easier to follow |
| Unified error handling | try/catch can handle async errors the same way as synchronous code |
| Better debugging | Stack traces output in a meaningful form |
| AGENTS.md as a safety net | Blocks the risk of unintended file modifications or dependency additions |
| Iterative validation | The diff review loop catches problems before commit |
| Risk | Mitigation |
|---|---|
| Automatic conversion failure for non-standard callbacks | List non-standard callbacks in AGENTS.md, set to Ask First |
| Parallel processing serialized → performance degradation | Check for missing Promise.all in /review |
| Error-ignoring patterns → uncaught rejections | Specify in Never section: no creating empty catch blocks |
| Out-of-scope modifications | Confirm with diff at every step, use workspace write sandbox |
| Insufficient test coverage | Adequate coverage before conversion is a prerequisite |
If test coverage is insufficient before conversion, there's no way to verify whether behavior has changed. No matter how well Codex and AGENTS.md perform, regressions cannot be caught without tests.
Conclusion
Three takeaways. First, unleashing Codex on a legacy codebase without AGENTS.md is like handing over a refactoring job without a map. Designing constraint boundaries upfront makes it clear when the agent should stop and ask for confirmation. Second, documenting what util.promisify covers and listing non-standard callbacks in advance prevents incorrect automatic conversions before they happen. Third, the discipline of validating every round with diff and review, then confirming behavioral equivalence with npm test before committing, is what lets you maintain both the speed of automation and the quality of the code.
I recommend starting cautiously in Suggest mode, and once you've built confidence in the conversion patterns, moving to Auto Edit.
References
- OpenAI Codex GitHub Official Repository — AGENTS.md
- AGENTS.md for OpenAI Codex: Complete Setup and Configuration Guide (2026) — The Prompt Shelf
- Codex CLI Skills & AGENTS.md Setup Guide 2026 — Agensi
- Codex CLI Cheatsheet: config, commands, AGENTS.md, best practices — Shipyard
- How to Build Your AGENTS.md (2026) — Augment Code
- Best AGENTS.md Examples and Templates for AI Coding Agents in 2026 — Promptessor
- Configuration Smells in AGENTS.md Files — arXiv
- Codex CLI agent review loop: the 2026 workflow — Ralphable Blog
- Codex CLI Code Review Workflows: /review, review_model — Codex Knowledge Base
- Codex CLI approval_policy — SmartScope
- Refactoring Legacy JavaScript with OpenAI Codex — Sumit Saha
- From Callback Hell to Async Heaven: Modern Node.js Patterns in 2025 — Medium
- Automating Callbacks to Async/Await Migrations for Mocha Tests — Marco Labarile
- Node.js util.promisify — Mastering JS
- Node.js Best Practices 2026 — Medium