Skip to content

Codex plugin

The Codex plugin gives Codex the same Loom integration as Claude Code: MCP tools, hooks that capture the session and load a primer, and the code shunt. Its scripts, skills and listing icon (assets/) are generated copies of the Claude Code plugin's; only the wiring in integrations/codex-plugin/hooks/hooks.json differs. Hooks and knobs are documented in claude-code.md; the exceptions are in What differs from Claude Code. What the plugin sends, and where, is in data handling.

Install

codex plugin marketplace add ClickHouse/loom        # or a path to a local clone
codex plugin add loom@loom

The plugin runs on macOS, Linux (glibc or musl; WSL 2 included) and Windows, on x86_64 and arm64. On macOS and Linux its hooks find Node through bin/loom-node; the header helper runs the node on PATH and falls back to the launcher. On Windows both run through cmd.exe and use the node on the PATH Codex starts with.

The plugin brings its own MCP entry (.mcp.json): the server URL with ?toolset=core,coding,tracker,tasks, authenticated by the plugin's header helper, bin/loom-mcp-headers.mjs. Do not add codex mcp add loom on top: an entry with the same name replaces the plugin's, and one under another name lists every tool twice.

Approve the hooks

Codex runs a plugin's hooks only after the user approves them, and again after an update that changes their commands. Run codex interactively once after installing and approve the Loom hooks; Codex records each approval as a trusted_hash under [hooks.state] in ~/.codex/config.toml. Until then the MCP tools work but sessions are not captured, including under codex exec.

The token

A single-user server on the same machine needs no token. Otherwise the helper and the hooks read LOOM_API_KEY, then ~/.loom/config.json, then ~/.loom/token; an agent (LOOM_CLIENT_ID with a workload token) uses LOOM_API_KEY when set, otherwise only the token it exchanges. On a server with LOOM_SELF_SIGNUP=open, the first session with no credential signs the machine up and writes ~/.loom/config.json itself (LOOM_AUTO_SIGNUP=off disables this). To use a token you already have:

mkdir -p ~/.loom && cat > ~/.loom/config.json <<'EOF'
{ "url": "https://loom.example.com", "token": "<writer JWT>" }
EOF
chmod 600 ~/.loom/config.json

Get the token from the dashboard's Settings → Tokens tab (<server>/settings) or with loom mint-token --subject you --org <org> --namespaces <ns> --roles writer. The hooks need writer; the tools can read with viewer. See tokens and grants.

The server

Codex plugins have no install options, so the URL is fixed in the plugin's .mcp.json (http://localhost:7777 as shipped) and in plugin-defaults.json for the hooks. A deployment that publishes its own marketplace sets both and keeps its name loom (.agents/plugins/marketplace.json): the skills and the header helper read only that marketplace's copy.

For another server on one machine, override the entry in ~/.codex/config.toml (the override replaces the plugin's entry whole, helper included), and put the same URL in ~/.loom/config.json for the hooks:

[mcp_servers.loom]
url = "https://loom.example.com/mcp?toolset=core,coding,tracker,tasks"
http_headers_helper = '''node -e "const f=require('fs'),p=require('path');if(process.versions.node.split('.')[0]<18)process.exit(1);const d=p.join(require('os').homedir(),'.codex','plugins','cache','loom','loom');const k=v=>v.split(/[.-]/).map(x=>x.padStart(9,'0')).join('.');const v=f.readdirSync(d).filter(x=>f.existsSync(p.join(d,x,'bin','loom-node'))).sort((a,b)=>k(a)<k(b)?-1:1).pop();import(require('url').pathToFileURL(p.join(d,v,'bin','loom-mcp-headers.mjs')).href)" || { l="$(ls -d "$HOME"/.codex/plugins/cache/loom/loom/*/bin/loom-node 2>/dev/null | sort -V | tail -1)"; sh "$l" "${l%/*}/loom-mcp-headers.mjs"; }'''

Codex runs the helper through sh -c on macOS and Linux and through cmd.exe on Windows, with a reduced environment (HOME or USERPROFILE, PATH, the locale and a few others), and does not expand ${PLUGIN_ROOT}. So the command starts with node -e, which both shells run: it loads the header helper of the newest installed loom@loom under the home directory's .codex. Under sh, when that step fails (the node on that PATH is missing or older than 18, or no installed copy is found), it falls back to the newest installed launcher (bin/loom-node), which also looks in a version manager's default directory under HOME and in the usual install directories; LOOM_NODE and variables such as NVM_DIR do not reach it. On Windows the node on PATH is the one that runs. With a custom CODEX_HOME, spell the path out. A Codex build that rejects Authorization from a header helper ("returned a reserved header") also needs bearer_token_env_var = "LOOM_API_KEY", with LOOM_API_KEY exported.

loom install --client codex --server-url https://loom.example.com --token <JWT> writes such an override with bearer_token_env_var and ?toolset=coding, and writes url and token to ~/.loom/config.json for the hooks (mcp-clients.md).

Where things live

What Where
MCP entry the plugin's .mcp.json; a [mcp_servers.loom] in ~/.codex/config.toml overrides it whole
Bearer LOOM_API_KEY → ~/.loom/config.json token → ~/.loom/token
Hook config ~/.loom/config.json {url, token, namespace?, org?}; LOOM_BASE_URL overrides; with no url, plugin-defaults.json
Plugin ~/.codex/plugins/cache/loom/loom/<version>/

What differs from Claude Code

Codex Effect
no Read / Grep tools; files are read through the shell tool the code shunt watches the shell tool (Bash, shell, exec_command, local_shell) for cat, rg, grep -r; the command may be a string or an argv array
edits go through apply_patch file context and blast radius read the target paths out of the patch text (*** Update File: / *** Add File:)
transcript_path may be missing or in Codex's own shape the hooks keep a turn journal: loom-run records each prompt, loom-conversation each reply (last_assistant_message) at Stop; the working-memory note, the run close, the conversation plane and session memory read it
no per-turn token counters in the payload loom-usage is not wired
SessionEnd hooks get about one second only fire-and-forget scripts run there (capture, run close, session memory, session brief); conversation turns are sent at Stop
no PostToolUseFailure event a PostToolUse result flagged as an error (is_error, isError or error) is observed as type error
the mentor-hint hook matches every tool it ignores results without a Loom hint and makes no request of its own
hooks run through $SHELL -lc, and through cmd.exe on Windows each command starts its script through bin/loom-node, which finds Node 18+ on PATH, under a version manager or in the usual install directories; on Windows commandWindows runs node from the system PATH. Codex reads it from 0.131; an earlier build on Windows runs the sh command through cmd.exe, where it fails, so Windows needs Codex 0.131 or later
identity run ids are cx-<session_id>, the default agent is codex, session memories carry source:codex

SessionStart fires on startup, resume, clear and compact; the primer is printed to stdout, which Codex adds as developer context. Every command in hooks/hooks.json names the client as the script's third argument (… PostToolUse codex).

Skills

The plugin carries loom-memory, loom-code, loom-mentor, loom-optimize, loom-session, loom-status and improve-loom-for-my-repo, described in claude-code.md. Codex loads them from the plugin; invoke one by name ($loom-status) or let its description trigger it.

AGENTS.md without the plugin

With the plugin, the skills above carry the contract and AGENTS.md needs none. Codex without the plugin reads the block in agent instructions from AGENTS.md at the repository root: the block names that repository's namespaces, so it does not belong in the global ~/.codex/AGENTS.md.

Headless

The hooks run under codex exec when the plugin is installed for the user running it. To point one run at a server without the plugin's entry:

codex exec --json \
  -c 'mcp_servers.loom.url="https://loom.example.com/mcp?toolset=coding"' \
  -c 'mcp_servers.loom.bearer_token_env_var="LOOM_API_KEY"' \
  "summarise what we decided about retries"

Normal coding work needs only ?toolset=coding; add &schema_profile=compact for a smaller tool listing (MCP reference). The headless bots guide covers scheduled and CI runs.

Checking it works

  • codex mcp list shows loom; the first tool call turns the token's row green on the dashboard's Settings → Tokens tab.
  • After one session: the dashboard's Conversations page shows the conversation (when logging is on for the namespace), run.get on cx-<session_id> shows the run's observations, and the next primer starts from the working-memory note.
  • Nothing arrives: run the loom-status skill. It checks the Node runtime and runs bin/loom-client-status.mjs from the plugin, with no Loom checkout or Python needed. loom doctor --client (operator CLI) adds the wiring checks.

Troubleshooting

Symptom Cause Fix
MCP connection fails with 401 no token found by the helper write ~/.loom/config.json, or export LOOM_API_KEY
MCP connection fails with 401, hooks work the helper finds no Node: it gets no LOOM_NODE put Node's directory on the PATH Codex starts with; $loom-status says
"returned a reserved header" this Codex build rejects Authorization from a helper override with bearer_token_env_var = "LOOM_API_KEY"
tools talk to the wrong server the bundled URL is not your deployment the config.toml override above, plus url in ~/.loom/config.json
tools listed twice a second MCP entry under another name remove it (codex mcp remove <name>)
node: not found from a hook the installed plugin predates its Node launcher codex plugin marketplace upgrade loom
a hook prints a loom: line about Node.js 18+ the launcher found no Node 18+, or only an old one LOOM_NODE=/path/to/node where Codex starts (macOS, Linux), or $loom-status
hooks write nothing viewer token, or the hooks read a different url writer token; check ~/.loom/config.json and LOOM_BASE_URL
Conversations page empty conversation logging is off for the namespace the Record conversations switch on the dashboard's Conversations page, or conversation.set_logging