Connect Codex to Nerudo
This guide gives the procedure to connect Codex to Nerudo and examine a change result.
Terms
| Technical noun | Meaning in this guide |
|---|---|
| CLI | Command-line interface. A program that receives commands from a terminal. |
| MCP | Model Context Protocol. A protocol for a client to use server tools. |
| native MCP server | The Rust MCP server in the Nerudo product repository. It uses standard input and standard output. |
| client | A program, such as Codex, that uses MCP tools. |
| agent | A program that prepares a change. Its agent name identifies its fork. |
| Git | The version-control program. |
| repository | A store of files and Git commit history. |
| checkout | The local files and Git data for a repository. |
| branch | A Git reference that identifies a line of commits. |
| commit | A Git record of a file tree, parents, and a message. |
| input commit | The commit at HEAD when the agent uses land. |
| trunk commit | The commit that the sequencer writes to trunk. |
| commit ID | The full Git hash that identifies a commit. |
HEAD | The Git reference for the selected commit in the checkout. |
| trunk ID | The service identifier for a trunk. |
| trunk | The repository branch that only the sequencer writes. Here, it is main. |
nerudo/main | The local reference that the sync tool sets to the trunk commit. |
| base commit | The common ancestor that the server calculates from HEAD and nerudo/main. |
| fork | The repository for changes from one agent. |
| change | One proposal to add a commit to trunk. A change has a stable change ID. |
| revision | A numbered version of a change. The server selects the revision number. |
| task | A work item with an ID such as T-12. |
| change context | The change record with the plan, decisions, alternatives, and task description. |
| sequencer | The Nerudo component that does checks of changes and writes them to trunk. |
| batch | A group of changes for one set of sequencer checks. |
| CI | Continuous integration. The checks that the sequencer does for a batch. |
| footprint unit | A group of paths that the repository rules identify for change order. |
| receipt | The result record for one change revision. |
| landing log | The sequence of records for changes that are on trunk. Hashes connect the records. |
| seal | A cryptographic approval signature from a person for one change revision and commit. |
| allowance | A cryptographic signature that gives an agent permission with limits. |
| agent bearer token | The operator-supplied AGENT credential for native agent operations. The server sends it in the HTTP Authorization header. |
| developer credential | A personal credential that starts with nrd_dev_. It gives access to selected account API operations. |
| agent key | A secret value that identifies one agent. It is not a person's private key. |
| environment variable | A named value that a program receives from its parent process. |
| launcher | A local Python program that reads credentials and starts nd. |
| configuration entry | The TOML section that tells Codex how to start the MCP server. |
| POSIX | An operating-system interface standard for the commands in this guide. |
| TOML | Tom's Obvious Minimal Language. The data format for the Codex configuration file. |
| JSON | JavaScript Object Notation. A data format for the credential file and HTTP records. |
| raw JSON record | The JSON result from an HTTP request, before MCP text conversion. |
| HTTP | Hypertext Transfer Protocol. HTTPS adds transport encryption. |
| URL | A network address for a resource. |
| API | Application programming interface. The HTTP routes that the Nerudo service supplies. |
Prerequisites and installation
The commands nd and nerudo use the same entry point.
Cargo uses nerudo for cargo run -p nerudo-cli.
The source files are main.rs and mcp.rs.
These files are in the Nerudo product repository.
The documentation repository does not contain the CLI source.
Nerudo controls repository changes through a sequencer. Only the sequencer writes to trunk. LandSeq is the previous product name. The service and the CLI do not accept LandSeq names. Storage identifiers and signed records keep their values.
This procedure is for a POSIX system with Git, Python 3, Rust, Cargo, and a Codex client with local MCP tools.
The Rust toolchain must be compatible with the dependencies in cf/Cargo.lock.
A Git author name and email address are necessary in the checkout.
The service must contain the repository nerudo/nerudo.
The service operator supplies an agent bearer token and, for a fork from a previous session, its agent key.
A personal developer credential gives no landing, Git transport, approval, or agent-delegation authority. The AGENT credential is necessary for this integration. An administrator token or a person's private key is not necessary.
-
Get the repository checkout from the operator.
-
Select the checkout directory in your terminal.
-
Install the package with this command:
bashcargo install --path cf/nerudo-cli --locked --bins -
Find
nd:bashcommand -v nd -
Find
nerudo:bashcommand -v nerudo -
Record the full path to
nd. -
Record the full path to
python3. -
Make sure that Git has the correct author name and email address:
bashgit var GIT_AUTHOR_IDENT
The package is nerudo-cli in the cf Cargo workspace.
Installation supplies both commands.
The CLI does not have submit or receipt subcommands.
The MCP server supplies the land and receipt tools.
Local credentials and launcher
The launcher starts this command with credentials in its process environment:
nd mcp --trunk nerudo/nerudo --agent <agent>The placeholder <agent> identifies the selected agent name.
The service address is https://nerudo.dev.
NERUDO_REPO identifies the full path to the authorized checkout.
Without this value, the server uses its working directory.
| Environment variable | Value |
|---|---|
NERUDO_WORKER | https://nerudo.dev. Without this value, the CLI uses http://127.0.0.1:8787. |
NERUDO_TOKEN | The agent bearer token from the service operator. |
NERUDO_REPO | The absolute checkout path. |
NERUDO_AGENT_KEY | Optional agent key. It replaces the agent key from the local file. |
On first fork creation, the native server receives an agent key.
Without an environment override, it keeps the key in ~/.nerudo/agents/<trunk-id>/<agent> with owner-only access.
An agent key is necessary for a fork from a previous session.
The operator can supply the key for a fork from a previous session.
-
Keep tokens and keys out of prompts, command arguments, shared configuration, and change context.
-
Keep credential files in a directory that is not in the checkout.
-
Make the local configuration directory:
bashmkdir -p "$HOME/.config/nerudo" -
Set directory access to mode
0700:bashchmod 700 "$HOME/.config/nerudo" -
Set the file creation mask:
bashumask 077 -
Make
~/.config/nerudo/credentials.jsonwith a local editor. -
Use this structure with your local values:
json{ "nd": "/absolute/path/to/nd", "agent": "codex_sol", "NERUDO_WORKER": "https://nerudo.dev", "NERUDO_TOKEN": "REPLACE_LOCALLY", "NERUDO_REPO": "/absolute/path/to/checkout"} -
For a fork from a previous session, add
NERUDO_AGENT_KEYwith the agent key from the operator. -
Set credential file access to mode
0600:bashchmod 600 "$HOME/.config/nerudo/credentials.json" -
Copy this launcher to
~/.config/nerudo/launch.py:
import jsonimport osfrom pathlib import Pathimport reimport statimport sysimport urllib.parseimport urllib.requestCREDENTIALS = Path.home() / ".config/nerudo/credentials.json"TRUNK = "nerudo/nerudo"def load_credentials(): fd = os.open(CREDENTIALS, os.O_RDONLY | os.O_NOFOLLOW) with os.fdopen(fd) as source: info = os.fstat(source.fileno()) if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) != 0o600): raise SystemExit("Credential file must belong to you, with mode 0600.") config = json.load(source) required = ("nd", "agent", "NERUDO_WORKER", "NERUDO_TOKEN", "NERUDO_REPO") if not all(isinstance(config.get(k), str) and config[k] for k in required): raise SystemExit("A required configuration value is absent.") if config["NERUDO_WORKER"] != "https://nerudo.dev": raise SystemExit("Worker address must be https://nerudo.dev.") if (config["NERUDO_TOKEN"] == "REPLACE_LOCALLY" or config["NERUDO_TOKEN"].startswith("nrd_dev_")): raise SystemExit("Set the operator-supplied agent bearer token locally.") if not re.fullmatch(r"[A-Za-z0-9_]+", config["agent"]): raise SystemExit("Agent name must contain only letters, digits, or underscores.") if not Path(config["nd"]).is_absolute(): raise SystemExit("The nd path must be absolute.") repo = Path(config["NERUDO_REPO"]) if not repo.is_absolute() or not repo.is_dir(): raise SystemExit("The checkout path must be an absolute directory path.") if "NERUDO_AGENT_KEY" in config: if not isinstance(config["NERUDO_AGENT_KEY"], str) or not config["NERUDO_AGENT_KEY"]: raise SystemExit("The optional agent key must be a nonempty string.") return configclass NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, request, response, code, message, headers, url): return Nonedef get_json(config, path): headers = { "Authorization": "Bearer " + config["NERUDO_TOKEN"], "User-Agent": "nerudo-guide/1", } if config.get("NERUDO_AGENT_KEY"): headers["X-Nerudo-Agent-Key"] = config["NERUDO_AGENT_KEY"] request = urllib.request.Request(config["NERUDO_WORKER"] + path, headers=headers) opener = urllib.request.build_opener(NoRedirect) with opener.open(request, timeout=120) as response: return json.load(response)def main(): config = load_credentials() args = sys.argv[1:] if args in (["mcp"], ["verify"]): allowed = ("HOME", "PATH", "LANG", "TMPDIR", "SSL_CERT_FILE", "SSL_CERT_DIR") env = {k: os.environ[k] for k in allowed if k in os.environ} for k in ("NERUDO_WORKER", "NERUDO_TOKEN", "NERUDO_REPO", "NERUDO_AGENT_KEY"): if k in config: env[k] = config[k] command = [config["nd"], args[0], "--trunk", TRUNK] if args == ["mcp"]: command += ["--agent", config["agent"]] os.chdir(config["NERUDO_REPO"]) os.execve(config["nd"], command, env) elif (len(args) == 3 and args[0] == "record" and re.fullmatch(r"[A-Za-z0-9._-]{1,64}", args[1]) and args[2].isdigit() and int(args[2]) > 0): suffix = f"changes/{args[1]}?rev={int(args[2])}" elif len(args) == 2 and args[0] == "log" and args[1].isdigit(): suffix = f"log?since={int(args[1])}" else: raise SystemExit("Use: launch.py mcp | verify | record CHANGE REV | log SINCE") trunk = TRUNK if "/" in trunk: trunk = get_json(config, "/v1/hub/repos/" + trunk)["trunk"] trunk = urllib.parse.quote(trunk, safe="") result = get_json(config, f"/v1/trunks/{trunk}/{suffix}") print(json.dumps(result, indent=2))if __name__ == "__main__": main()The mcp and verify modes start native nd commands.
The record and log modes make HTTP GET requests.
They are launcher modes, not nd subcommands.
The launcher sends no credentials in command arguments.
It stops on HTTP redirects to keep credentials at the selected origin.
The route GET /v1/hub/repos/nerudo/nerudo gives the trunk ID for this public repository.
For a private repository, the agent uses the trunk ID from the operator.
The native CLI uses an administrator token for private repository name resolution.
- For a private repository, replace
TRUNKin the launcher with the supplied trunk ID. - If you cannot get access to a fork from a previous session, tell the operator.
- Do not put the operator's key output in the conversation.
- Do not give the agent an administrator token or a person's signature key.
Codex configuration
Codex uses a TOML configuration entry for each MCP server.
The official OpenAI documentation gives information about the command, args, and timeout fields.
The entry below uses the server name nerudo.
It contains paths, not credentials.
-
Add this entry to your local
~/.codex/config.toml:ini[mcp_servers.nerudo]command = "/absolute/path/to/python3"args = ["/absolute/path/to/.config/nerudo/launch.py", "mcp"]startup_timeout_sec = 60tool_timeout_sec = 180 -
Replace the example paths with the recorded local paths.
-
Stop the client.
-
Start the client.
-
Examine its MCP tool list for
nerudo. -
Make sure that the tools include
sync,rules,land, andreceipt.
The native tool list also contains tasks, claim, new_task, in_flight, status, context, and cleanup.
It contains comment, mentions, request_review, react, and release_propose.
The server also lists request_approval when the client can show the MCP approval form.
Prepare and send a change
These examples show MCP tool names and argument objects.
They are not terminal commands.
sync fetches trunk into nerudo/main without a branch switch or rebase.
The repo argument for a tool can select only the configured checkout or a directory below it.
- Use
taskswith{}. - If no applicable task is available, use
new_taskwith{"title":"Change title","body":"Change purpose"}. - Select the applicable task ID.
- Use
claimwith{"task":"T-12"}for that task. - Use
syncwith{}. - Use
ruleswith{}. - Use
in_flightwith{}. - Examine the rules and changes that wait.
- If one more change is for the same code, use
contextwith itschange_id. - Make a branch from trunk:
git switch -c T-12-change-name nerudo/main- Make the necessary changes.
- Do the applicable checks.
- Examine the difference with
git diff. - Add the necessary paths with
git add <path>. - Make one commit with
git commit. - Give the cause of the change in the commit message body.
- Make sure that the last commit paragraph contains the applicable task trailer, such as
Task: T-12. - Record the full commit ID:
git rev-parse HEAD- Examine the commit:
git show --stat HEAD- Make sure that this command gives no output:
git status --porcelain- Make sure that
HEADidentifies the correct commit. - Use
landwith the applicable values:
{ "change_id": "T-12-change-name", "task": "T-12", "context": "Plan: Add the requested guide. Decision: Use the native MCP server. Rejected alternative: A separate adapter adds unnecessary code.", "wait_s": 50}- Record the change ID and revision number from the result text.
- Use
receiptwith those result values:
{"change_id":"T-12-change-name","rev":1}- If the revision from the result is not
1, replace the example revision with the number from the result.
The native land tool reads HEAD, fetches trunk, and calculates the base commit with Git merge-base.
It selects the revision internally.
Its arguments do not include a commit ID, base commit, or revision number.
It sends the commit to the agent fork and sends a request to add the change to trunk.
It keeps supplied change context on the fork's nerudo/context branch.
land usually waits a maximum of 50 seconds for a receipt.
The maximum wait_s value is 600 seconds.
The client timeout can stop the tool request first.
A client timeout does not show that the server stopped the change.
The text shows only the first part of each commit ID.
The raw JSON record supplies full commit IDs.
Receipt formats and result checks
The native tools land and receipt give MCP text results with this structure:
{"content":[{"type":"text","text":"LANDED: ..."}],"isError":false}This example shows only the first part of the result text.
isError: false shows that the tool call did not give an error.
It does not show that the change is on trunk.
The text can also show QUEUED, PARKED FOR A HUMAN, or an ejection message.
The HTTP request GET /v1/trunks/<trunk-id>/changes/<change-id>?rev=N gives a raw JSON record.
The record has state, req, seq, and trunk_commit fields directly.
It does not have an outer receipt field or a landed boolean.
req contains change_id, rev, commit, base, and fork.
A result with state: "landed" identifies the landed revision.
-
Get the record with the change ID from the result and revision number:
bashpython3 "$HOME/.config/nerudo/launch.py" record T-12-change-name 1 -
Make sure that
req.change_idandreq.revagree with the values fromland. -
Compare
req.commitwith the full commit ID recorded beforeland. -
Make sure that
stateislanded. -
Record the sequence number in
seq. -
Record the full trunk commit ID in
trunk_commit.
The sequencer makes a new trunk commit with its trailers. The input commit ID and trunk commit ID can be different. An ancestor check of only the input commit cannot show that the change is on trunk.
Examine Git and the landing log independently
-
Use the MCP
synctool with{}. -
Make sure that Git contains the recorded trunk commit:
bashgit cat-file -t <trunk-commit>The output must be
commit. -
Make sure that trunk contains that commit:
bashgit merge-base --is-ancestor <trunk-commit> nerudo/mainExit code
0shows that the commit is an ancestor of the last trunk commit. -
Examine the trunk change:
bashgit show --format=fuller <trunk-commit> -- <changed-path> -
Compare the files with the necessary change.
-
Get the landing log with
SINCEequal to the recorded sequence number minus one:bashpython3 "$HOME/.config/nerudo/launch.py" log SINCE -
Select the log entry with the recorded sequence number.
-
Compare its
change_id,rev, andcommit_inwith the recorded change ID, revision number, and input commit ID. -
Compare its
commit_outwith the recorded trunk commit ID. -
Do the native log and signature checks:
bashpython3 "$HOME/.config/nerudo/launch.py" verify -
Make sure that the command gives exit code
0.
The launcher verify mode starts nd verify --trunk nerudo/nerudo with the local credentials.
That command reads trunk independently.
It does checks of the landing log and approval signatures.
The log route gives at most 500 entries after since.
A subsequent change can move trunk to a newer commit than the recorded trunk_commit.
Retries and human approvals
| Result text | Action |
|---|---|
LANDED | Do the result checks for the revision from the result. Use sync before the next change. |
QUEUED | Use receipt again with the same change_id and rev. |
PARKED FOR A HUMAN | Stop. Tell the user the cause and the approval command from the receipt. |
CLOSED: conflict | Use sync. Rebase with git rebase nerudo/main. Repair the conflict. Do the checks again. |
EJECTED BY CI | Use sync. Rebase on nerudo/main. Repair the reported errors. Do the checks again. |
WAITING FOR A PICK | Stop work on this change. Wait for a person to select a change. |
CLOSED, NOT PICKED | Stop work on this change. |
REPLACED | Get the newer revision number. Use receipt with that number. |
| Other ejection | Examine the cause before one more land request. |
- After a conflict or CI repair, record the new
HEADcommit ID. - Use
landagain with the samechange_idand task ID. - Record the new revision number from its result.
- Use
receiptwith that new revision number. - If a request times out, use
receiptbefore one morelandrequest. - If the revision number is unknown, use
receiptwith onlychange_idto find the last revision number. - Then use
receiptwith that revision number. - Compare the raw record's
req.commitwith the recordedHEAD. - If no record is available, do an investigation of the request before one more
landrequest. - Do not change
HEADuntil you know the result. - Do not sign an approval, seal, allowance, or release as the user.
- Do all necessary approval procedures and checks.
- If the user does not accept an approval form, do not show the form again without the user's instruction.
A missing record does not show that a previous request cannot finish.
For the same commit that waits, land reports the same revision.
For a different commit, land can replace a revision that waits.
For an ejected revision, land selects the next revision number.
No new revision is possible for a change on trunk.
The API client sends the HTTP request again for 503 responses with Retry-After at most three times.
It does not automatically send the request again for all network errors.
The optional request_approval tool shows a form to the person.
Only the person's accepted form with approve: true gives permission for the signature.
An agent does not complete the form for the person.
The language review record gives the language scope and inspection limits.