fix(probe): Codex replies over the local route inside its sandbox; init --uninstall restores ~/.codex
Fixes two problems the agent-relay@15.0.0-next.4 end-to-end test of relay init --tools codex found.
1. Codex could not reach the session socket from its sandbox, then replied through the wrong route
Root cause. On macOS Codex's default sandbox (workspace-write/read-only, network off) is a Seatbelt profile that denies every Unix-socket connect. The injected reply hint tells Codex to run the probe's own client (agent-relay-probe relay socket-request --socket <session socket> …, piped from printf; relay read <ref> for pointer messages). Both connect to the core's session socket, so they failed inside the sandbox with "Agent Relay's session socket is unavailable". The hint for a session with unknown MCP access also names send_relay_message, and Codex used the account-level codex_apps.agent_relay_in_chatgpt.send_relay_message connector as a fallback. That connector sends as the person's Cloud account into a different workspace.
Mechanism chosen: a Codex exec-policy rules file, not a sandbox widening. I checked this against Codex 0.163.0 (rust-v0.163.0 source, plus the installed binary):
[sandbox_workspace_write]only haswritable_roots,network_access(all or nothing),exclude_tmpdir_env_varandexclude_slash_tmp. It has no Unix-socket allowance.- A path allowlist (
network.allow_unix_sockets,sandboxing/src/seatbelt.rs) does exist, but only in[permissions.<name>.network]permission profiles (selected withdefault_permissions, which replaces the user's whole sandbox setup) or in managed network policy. Both are too invasive forrelay initto write into someone's config. - In
core/src/exec_policy.rs,Decision::AllowgivesExecApprovalRequirement::Skip { bypass_sandbox: true }when every parsed command segment matches anallowrule from$CODEX_HOME/rules/*.rules.
So while codex_stop_hook is on, the core also writes ~/.codex/rules/agent-relay.rules (agent-relay-dev.rules for Dev) with exactly two prefix_rules:
[<this probe>, "relay", "socket-request", "--socket", <this core's session socket>][<this probe>, "relay", "read"]
Nothing else leaves the sandbox and no network access is granted. socket-request only allows its own method/path list and checks that the peer runs as the same uid. The file starts with an Agent Relay header, and a file of that name without the header is never rewritten or removed. The core rewrites it at start, so a moved or updated probe is picked up. codex execpolicy check confirms both rules match and that a different --socket does not.
Tradeoff: the probe's two client commands run unsandboxed rather than inside a sandbox with a socket hole punched in it. They are our own binary, limited to our own socket, so this is far narrower than network_access = true. A command that is not exactly this shape (another path to the probe, extra pipes) still gets Codex's normal escalation prompt.
Other changes:
relay socket-request --body <text>. Reply hints (DM, Relay Connect, channel) are now one command with noprintf … |pipe, because Codex only bypasses the sandbox when every segment of a command matches a rule.- Every reply hint now ends with this line:
[route: answer only as shown above, through this computer's Agent Relay; never through any other Agent Relay connector or app (such as Agent Relay in ChatGPT), which sends as someone else. If that fails, say so instead of sending another way.]
The session preamble says the same thing.
2. init --uninstall left Codex leftovers
Root cause. Turning codex_stop_hook off only rewrote hooks.json without our hook. That left {"hooks":{}} behind, and that rewrite also created a .relay-desktop-backup copy of Relay's own file, because the backup was taken on every first write. The trust entries Codex's app-server had recorded in config.toml (hooks.state."<hooks.json>:stop:0:0", …) were never removed.
Fix. Turning it off now puts ~/.codex back:
- Our hook's keys are listed through Codex's app-server (
hooks/list) while the hook is still inhooks.json. The hook is then taken out, and exactly thosehooks.statekeys are cleared withconfig/value/write(valuenull).[hooks.state]or[hooks]is removed too, but only once it is empty. The person's own trust entries stay. - The backup is now taken only of a file that does not already hold the hook, so it is always the person's original. If they haven't changed the file since, it is restored byte for byte and the backup is deleted. A
hooks.jsonthe core created is deleted once it is empty. An earlier build's self-backup is dropped. - A
config.tomlthat trusting the hook created (recorded ascodex-config-createdin the core's data dir) is deleted once nothing else is in it. - The rules file is removed.
rules/is removed only if it is then empty.
Tests
- New Rust tests in
codex/stop_hook.rs:- fresh install → off leaves the Codex home tree identical
- a person's own
hooks.json(non-canonical formatting),config.tomlwith their own trust entry, andrules/default.rulessurvive byte for byte - a
hooks.jsonedited after install keeps the edit - an earlier build's self-backup is removed
- the rules allow only the two commands
- a foreign
agent-relay.rulesis left alone
- An
#[ignore]d live test,real_codex_untrust_puts_config_toml_back_byte_for_byte, runs against the real Codex 0.163 app-server and passes. cargo test --no-default-features --features production-binaries: lib 2361 passed / 0 failed on a clean run. Every integration test binary passes, includingrelay_agents(114) andrelay_outbound. On a second run, three timing tests (devin/grok startup and leader) flaked under load. They pass when re-run alone and are untouched here.cargo clippy --all-targets: no warnings in the touched files.init.test.ts: 95/95.tsc --noEmitforpackages/cli: clean. eslint oninit.ts/init.test.ts: 0 errors (4 warnings, all pre-existing).
End-to-end proof (macOS, Codex 0.163.0, default sandbox, no approval)
Setup was isolated: HOME, AGENT_RELAY_HOME, AGENT_RELAY_INSTALL_DIR and CODEX_HOME under /tmp/arcx, with only auth.json copied in. The project was ~/arcx-project. I used the CLI built from this branch with AGENT_RELAY_PROBE_PATH pointing at the locally built probe, and the local engine on port 8797.
relay init --yes --tools codexwrote the hooks,rules/agent-relay.rulesand twohooks.statetrust entries.- Interactive
codex(default sandbox, project trusted through Codex's own prompt) auto-joined ascodex-arcx-project-reply-just. relay message dm send codex-arcx-project-reply-just "… reply … pong" --as alicesent message241608806540062720. The injected text ended with the new[route: …]line.- Codex ran
agent-relay-probe relay socket-request --socket '…/run/relay.sock' --method POST --path '/send?address=alice@direct&reply_to=241608806540062720' --body 'pong'with no escalation request and no approval prompt. The result was{"ok":true, "message_id":"241608826450423808"}.relay message inbox check --as aliceshowspongfrom the Codex session. - Control:
codex sandbox -- <probe> relay socket-request … --path /agentsfails with "Agent Relay's session socket is unavailable", so the sandbox still blocks the socket and only the rule lets the command through. relay init --uninstall:hooks.json,rules/and everyhooks.stateentry are gone, with no backup left.config.tomlkeeps only Codex's own[tui]/[projects]entries, and the marker is removed.- Pre-existing user files: hand-written
hooks.json,config.toml(with comments) andrules/default.rules, then init → uninstall.cmpshows all three identical.
All processes, tmux servers, the Codex app-server daemon and the directories were removed afterwards.
🤖 Generated with Claude Code
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Need help on this PR? Tag @codesmith-bot with what you need.
Agent Relay sessions
claudesession8ab900cc· opened viagh pr create· last active 2026-10-29