🔌 Project 2: MCP and hooks between Claude and Codex
Tools travel, events don't. This project registers in Codex the MCP servers that today only Claude can see (magnific, metricool, klingai, cerebro-vip), unlocks the 15 adapter skills that depended on them, and then tackles the part that has no equivalent: hooks. Codex has PostToolUse and Stop, but no SessionStart — and SessionStart is exactly where fable-mindset lives. Whatever can't become an event becomes text to be read, and the 7 subagents become role skills.
🎯 The project on one screen
~/.codex/AGENTS.md and 7 role skills in .agents/skills/.codex mcp list shows the servers; an adapter skill runs in Codex; grep -R 'sk-' ~/.codex finds nothing; and a fresh Codex session can take on one of the 7 roles by citing the file.🧾 Inventory: who has which tool
Before touching anything, take inventory. The 2026-09-14 diagnosis measured both runtimes side by side, and the result for MCP is short and brutal: Claude has magnific and metricool globally, plus two MCP servers registered per project — wifi → klingai and 2cerebrox → cerebro-vip. Codex has none. It's not "has fewer": it's zero. Every skill of yours that calls one of these tools simply doesn't exist on the Codex side, no matter how well written it is.
It's worth separating two axes people often confuse. MCP is a tool: an external process the agent calls to do something in the world (generate an image, schedule a post, check a metric). A hook is an event: a harness trigger that fires on its own at a given point in the session lifecycle. The first is a protocol contract, so it travels between runtimes. The second is a harness implementation detail, so it does not travel — and when it doesn't, the only way out is to turn the hook's effect into something the agent reads.
The left panel is the good news: MCP is a protocol, so the same server serves Claude and Codex without duplicating anything. The right one is the bad news: a hook is an implementation of the harness, and it dies at the border. The purple block at the bottom is this project's way out — whatever can't be an event has to become required reading.
📊 The measured inventory
| Scope | Claude Code | Codex CLI |
|---|---|---|
| Global MCP | magnific, metricool | none |
| Per-project MCP | wifi → klingai, 2cerebrox → cerebro-vip | none |
| Hooks | 2 SessionStart (context-mode, fable-mindset) | PostToolUse + Stop (impeccable) |
| Subagents | 7 in ~/.claude/agents | no equivalent |
| Skills blocked by this | — | 15 adapter + 2 native |
Goal: reproduce the inventory on your machine before changing anything, so you have a baseline.
# MCPs that Claude sees (global + per project)
claude mcp list
# expected on this machine: magnific, metricool
# per-project MCPs live in each repo's .mcp.json
ls ~/projetos/wifi/.mcp.json ~/projetos/2cerebrox/.mcp.json 2>/dev/null
# MCPs that Codex sees
codex mcp list
# expected TODAY: empty list — this is the project's gap
# hooks on both sides
grep -rho '"SessionStart"\|"PostToolUse"\|"Stop"' ~/.claude/settings.json ~/.claude/plugins/cache 2>/dev/null | sort | uniq -c
grep -o '"SessionStart"\|"PostToolUse"\|"Stop"' ~/.codex/hooks.json | sort | uniq -c
How to verify: write down both lists in context/overview.md of the project you're working on. By the end of this module, codex mcp list must no longer be empty — and that's the only metric that matters in topic 2.
Golden rule of the inventory: don't trust your memory. An MCP "you swore you had registered" that doesn't show up in list is an MCP that doesn't exist for the agent. The same goes for hooks: if it's not in the settings file, it doesn't fire — and you'll spend an hour debugging behavior that was never turned on.
Key concepts
An external tool spoken to over a protocol; any runtime that speaks the protocol can use it.
An event hook of the harness; it's not a protocol, it's a local implementation.
An MCP can apply to the whole machine or only inside one repo.
The state measured before the change; without it there is no "it got better".
🔑 codex mcp add without copying a single secret
The command itself is simple: codex mcp add <nome> -- <comando>. What calls for care is what goes along with it. This machine's global rule is explicit: API keys always live in ~/projetos/openpcbotv2/.env or ~/projetos/wifi/.env, are loaded at runtime and are never duplicated anywhere else. Registering an MCP by pasting the key into the command argument breaks that rule twice: it creates a second copy of the secret and writes it to a config file you will eventually sync, version or paste into a chat.
The way out is indirection: the registered command is not the server, it is a three-line wrapper that loads the .env and only then execs the server. The loading pattern is set -a; source .env; set +a — set -a turns every assigned variable into an exported environment variable, source reads the file, set +a switches it off. The secret passes through process memory and never touches the Codex configuration.
Find the real command
In Claude, each MCP already has a startup command. Copy the command, not the key.
Write the wrapper
One .sh per server in ~/.local/bin/, with set -a; source ...; set +a and exec at the end.
Register it pointing to the wrapper
codex mcp add magnific -- ~/.local/bin/mcp-magnific.sh. The configuration stores a path, not a secret.
Prove nothing leaked
A grep over the ~/.codex tree looking for key prefixes. Zero results is the acceptance test.
Goal: register magnific and metricool in Codex referencing the keys in .env, without storing any value.
# 1. wrapper that loads the .env at runtime and hands off to the server
mkdir -p ~/.local/bin
cat > ~/.local/bin/mcp-magnific.sh <<'SH'
#!/usr/bin/env bash
set -euo pipefail
set -a; . "$HOME/projetos/openpcbotv2/.env"; set +a # loads, does not copy
exec npx -y @magnific/mcp-server # replace with your MCP's real command
SH
chmod 700 ~/.local/bin/mcp-magnific.sh
# 2. register in Codex: the config stores a PATH, never a key
codex mcp add magnific -- "$HOME/.local/bin/mcp-magnific.sh"
codex mcp add metricool -- "$HOME/.local/bin/mcp-metricool.sh"
# 3. check
codex mcp list
How to verify: codex mcp list is no longer empty and shows both names. Then open codex in any folder and ask "list the available MCP tools" — the names have to show up in the answer, not just in the file.
Goal: prove that no secret ended up in the configuration — this is the test that closes the topic.
# search for typical key prefixes inside the Codex config
grep -rIl -e 'sk-' -e 'API_KEY=' -e 'TOKEN=' ~/.codex 2>/dev/null
# expected: NO output lines
# check that the wrapper is not readable by other users
stat -c '%a %n' ~/.local/bin/mcp-*.sh
# expected: 700 on all of them
# check that the .env is still the single source
grep -c '=' ~/projetos/openpcbotv2/.env # count only; never print the contents
How to verify: the first command must return silently. If it prints any path, stop everything: some key was copied. Remove the registration with codex mcp remove <nome>, rotate the key, and redo it through the wrapper.
✓ The right way to carry the key
- ✓The wrapper runs
sourceon the.envat runtime, on every startup. - ✓The Codex configuration stores only the wrapper's path.
- ✓Rotating the key means editing a single file; nothing else has to change.
- ✓
chmod 700on the wrapper and.envkept outside any published repo.
✗ The way that creates debt
- ✗Pasting the key into
codex mcp add ... --env KEY=sk-...: it becomes plain text on disk. - ✗Duplicating the
.envinto~/.codex"just to make it easier". - ✗Exporting the key in
.bashrc: it starts leaking to every process on the machine. - ✗Printing the value to "check that it is right" — the terminal ends up in the history and the session log.
The two per-project MCPs: klingai (in wifi) and cerebro-vip (in 2cerebrox) follow exactly the same pattern, but are registered from inside the repo so they keep a local scope. The question that decides scope is simple: "will any other project want to call this?" If the answer is no, keep it per project — a global MCP is attack surface and context noise in every session.
Key concepts
A short script that loads the environment and runs exec on the real server.
set -aMakes source export everything; set +a switches it off right after.
Point to the source of the secret; never create a second source.
The acceptance test is a grep that finds nothing — verified absence.
🔓 The 15 adapter skills unlock later
The skills audit sorted 89 Claude-only skills into four classes. 72 are reusable — plain Markdown and scripts that port unchanged. 15 are adapter skills: they depend on an MCP or a Claude plugin, so they only make sense in Codex after the matching tool is registered. Two are native (they depend on a SessionStart hook) and one has no SKILL.md.
That is why this project comes before the bulk skill migration. Porting an adapter skill while its MCP is missing gives you the worst possible outcome: it gets discovered, it gets picked, and it fails halfway through — after the agent has already promised the user it would generate the video. Tool first, skill second. The order isn't a preference; it's what separates "not installed" from "installed and broken".
🧩 The 15 adapter skills, by dependency
Eight of them are printing-press variants, which count as a single dependency. Solving three MCPs (heygen, comfy and the espiona-ads one) unlocks the whole block.
✓ Ready to port now
- ✓Only reads and writes files, or calls binaries already on the machine.
- ✓The
SKILL.mddoesn't mention anymcp__…in the body of its instructions. - ✓Helper scripts are bash or node with no plugin dependency.
- ✓The whole happy path runs without network access.
✗ Waits for the matching MCP
- ✗The
SKILL.mdsays "callmcp__magnific__images_generate". - ✗Depends on a Claude plugin (superpowers, context-mode, claude-mem).
- ✗Depends on a subagent: "dispatch to
analista-neutro". - ✗Assumes
AskUserQuestionor another Claude-only UI feature.
Goal: automatically sort your skills into "reusable" and "adapter", so you know what to port today.
# adapter = the SKILL.md mentions an MCP, plugin or subagent
cd ~/.claude/skills
for d in */; do
s="${d%/}"
if grep -qE 'mcp__|AskUserQuestion|subagent_type|superpowers:|context-mode:' "$s/SKILL.md" 2>/dev/null; then
echo "ADAPTADOR $s"
else
echo "portavel $s"
fi
done | sort | tee ~/classificacao-skills.txt | awk '{print $1}' | uniq -c
# show only the blocked ones, and which dependency blocks them
grep '^ADAPTADOR' ~/classificacao-skills.txt | awk '{print $2}' | while read s; do
echo "== $s"; grep -ohE 'mcp__[a-z0-9_]+' "$s/SKILL.md" | sort -u
done
How to check: the count should match the order of magnitude of the diagnosis — dozens of portable skills against about 15 adapters. If you get 80 adapters, your grep is catching mentions inside examples; adjust it to look only at instruction lines.
Sequencing tip: after registering magnific and metricool in topic 2, come back to this list and move to "portable" only the skills whose only dependency was one of those two. The heygen and comfy ones stay blocked until you register the matching MCPs — and that is a task for another session, not an exception to open now.
Key concepts
A skill whose value depends on a registered external tool.
Worse than not having it: the skill is picked and breaks after the promise.
The order that avoids installing something broken in Codex.
8 printing-press variants unlock with a single MCP.
🪝 Hooks: the real mapping between the two
Here the news is mixed. Claude has two SessionStart hooks — context-mode and fable-mindset — and a good part of the behavior you think of as "the way Claude works" comes from them. Codex has hooks too, but for other moments: this machine's ~/.codex/hooks.json registers a PostToolUse (matcher Edit|Write|apply_patch, 5s timeout) and a Stop (30s timeout), both calling the same hook.mjs from impeccable. What doesn't exist is the opening hook: SessionStart has no equivalent in Codex.
The line is a session's lifecycle. The two cyan dots (PostToolUse and Stop) already exist in both runtimes and take no work. The purple dot (SessionStart) exists only in Claude — and the dashed arrow pointing down is the only possible answer: swap the automatic event for mandatory reading at the start of the session.
🗺️ The mapping table
| Event | Claude | Codex | What to do |
|---|---|---|---|
SessionStart | ✓ 2 hooks | ✗ doesn't exist | Turn it into text in AGENTS.md + a prime skill |
PostToolUse | ✓ | ✓ impeccable | Nothing; check the matcher |
Stop | ✓ | ✓ impeccable | Nothing; check the 30s timeout |
| Plugins | ✓ 7 | ✗ 1 (github) | Claude leftover; doesn't migrate |
Goal: read what is actually registered on both sides, instead of assuming.
# which events Codex has registered on this machine
python3 -c "import json;print(list(json.load(open('$HOME/.codex/hooks.json'))['hooks']))"
# expected: ['PostToolUse', 'Stop']
# the matcher and timeout of each one
grep -E '"matcher"|"timeout"|"statusMessage"' ~/.codex/hooks.json
# does the target of both hooks exist? (the hook is written to fail silently if it doesn't)
ls -l ~/.agents/skills/impeccable/scripts/hook.mjs
# what Claude fires on SessionStart — this is the one with NO counterpart
grep -rl '"SessionStart"' ~/.claude/settings.json ~/.claude/plugins/cache 2>/dev/null
How to verify: the Codex list comes back with two names, and neither is SessionStart. If the ls of hook.mjs fails, the Codex hooks are registered but inert — the [ ! -f ... ] || at the start of the command makes them exit quietly.
Why the hook fails silently on purpose: note the form [ ! -f "…/hook.mjs" ] || node "…/hook.mjs". If the file disappears, the command returns success and the session carries on. That's desirable for a cosmetic hook like impeccable's, and terrible for a hook that loads context: you'd think you had read AGENTS.md when you hadn't. It's one more argument against relying on an event for anything that must be guaranteed.
Key concepts
Opening hook; exists only in Claude, and it's the one that hurts most to lose.
Filter for which tools trigger PostToolUse.
A hook written not to break the session when its target disappears.
The only possible translation when the hook doesn't exist on the other side.
📝 fable-mindset becomes a text section in AGENTS.md
fable-mindset is one of the two skills classified as native: it exists as a SessionStart hook that injects a behavior playbook at the start of every Claude session. Since Codex has no SessionStart, there's nowhere to port the mechanism. But the playbook's content is just text — and text is the most portable thing there is. The conversion is direct: what used to be injected by an event becomes a section of ~/.codex/AGENTS.md, read because the file is read.
The playbook distilled from this analysis fits in a few lines, and its core is what you could call the pacing rule: think before acting, close the loop by verifying. Two halves that hold each other up. The first prevents the session that starts editing files before understanding the problem; the second prevents the session that declares "done" without running anything. Written like this, without harness jargon, it works in any runtime — including the dsh of the next project, which has no hooks at all.
Goal: add the pacing rule to ~/.codex/AGENTS.md, replacing the hook with reading.
# back up before touching the global base (Project 1 created this file)
cp ~/.codex/AGENTS.md ~/.codex/AGENTS.md.bak-$(date +%Y%m%d)
cat >> ~/.codex/AGENTS.md <<'MD'
## Pacing rule (was the fable-mindset hook in Claude)
- **Think before acting.** Before the first edit, state in one sentence
what the problem is and what the smallest change that solves it is. If
you don't know, read more; don't start editing to find out.
- **Close the loop by verifying.** Every change ends with a command that
proves the result (test, `grep`, `status`, readback) and with the output
pasted in the answer. "It should work" doesn't close the loop.
- **One fix at a time.** If the answer was to rewrite everything,
a guard was probably missing — record that in FALHAS.md.
MD
# check that it went in and that the file is still short
tail -14 ~/.codex/AGENTS.md
wc -l ~/.codex/AGENTS.md
How to verify: run codex exec "Before editing any file, what must you say first? Cite the source." from ~. The answer must mention the sentence about the problem and the smallest change, citing AGENTS.md. If it answers well without citing the source, that's the model getting lucky, not reading — and it doesn't count as acceptance.
✓ Translates well into text
- ✓Behavior playbook: pacing, order of work, what to do before declaring done.
- ✓Hard prohibitions and style preferences.
- ✓Order for reading files at the start of the session.
- ✓Acceptance criteria and handoff format.
✗ Doesn't translate into text
- ✗What the hook computes: mining 2.3 GB of JSONL to generate the playbook.
- ✗Actually blocking an action — text asks, a hook prevents.
- ✗Guaranteed injection: text can be pruned from context in a long session.
- ✗Automatic routing of calls to a subagent.
The honest cost of the conversion: a hook is a guarantee, text is a request. Swapping one for the other costs you determinism — and that's exactly why the pacing rule has to be short. Three bullets the model reads at the start of every session are worth more than forty lines it will skim past without reading. The same logic applies to silver-platter, the other native skill.
Key concepts
Depends on a harness feature; only the content migrates.
Think before you act; close the loop by verifying.
A hook always runs; text depends on being read and obeyed.
A long instruction isn't stronger, it's more ignored.
🎭 The 7 subagents become role skills
Claude has 7 subagents in ~/.claude/agents: advogado-do-diabo, analista-neutro, estrategista-otimista, mestre-do-conselho, diretor-ecossistema, web-research-assistant and triple-x-responder. Codex has no equivalent — the diagnosis is categorical: subagents stay behind as Claude residue. But each of these files is, at heart, a role described in prose: what the agent takes on, what it looks for, the output format. That becomes a skill.
The destination is .agents/skills/ — the same folder Codex already scans and that sync-skills.sh mirrors at install time. What you lose is parallelism: in Claude, three council members run at the same time in separate contexts. With a role skill, the same agent takes on the roles in sequence, in the same context — cheaper, slower, and with contamination between the voices. It's a real loss, and it's worth saying so in the skill description instead of pretending they're equivalent.
Goal: convert the 7 subagents into role skills, preserving the prompt body and swapping only the header.
mkdir -p ~/.agents/skills
for a in advogado-do-diabo analista-neutro estrategista-otimista \
mestre-do-conselho diretor-ecossistema web-research-assistant \
triple-x-responder; do
src=~/.claude/agents/$a.md
[ -f "$src" ] || { echo "missing: $a"; continue; }
mkdir -p ~/.agents/skills/papel-$a
{
echo "---"
echo "name: papel-$a"
echo "description: Takes on the role of $a. Use when the task explicitly asks for that voice."
echo "---"
echo
echo "> Role derived from the Claude Code subagent \`$a\`."
echo "> There is NO parallel execution here: take on the role in the current context and"
echo "> make it clear in the response when you are speaking for it."
echo
sed '1{/^---$/!b}; 1,/^---$/d' "$src" # strip only the old front-matter
} > ~/.agents/skills/papel-$a/SKILL.md
echo "papel-$a"
done
How to verify: ls ~/.agents/skills | grep -c '^papel-' returns 7. Then, codex exec "Use papel-advogado-do-diabo to attack this decision: migrate everything at once." — the response must cite the skill file and hold the devil's-advocate voice from start to finish.
✅ Acceptance criteria (check off with evidence)
- ☐
codex mcp listshows magnific and metricool. Evidence: command output. - ☐
grep -rIl -e 'sk-' -e 'API_KEY=' ~/.codexreturns nothing. Evidence: empty output pasted into the handoff. - ☐An adapter skill that depended only on magnific runs end to end in Codex. Evidence: the generated artifact.
- ☐The pacing rule is in
~/.codex/AGENTS.mdand is cited in acodex exec. Evidence: the response with the citation. - ☐7
papel-*skills in~/.agents/skills, and one of them was exercised. Evidence:ls+ the role's response. - ☐Nothing broke in Claude:
claude mcp listand the SessionStart hooks remain the same. Evidence: comparison with the baseline from topic 1.
⚠️ Risks of this project
- •A key copied into
~/.codexin a hurry — the costliest risk on this page. - •MCP registered globally when it should be per project: context noise in every session.
- •Adapter skill ported before the MCP: fails mid-execution.
- •Assuming a role skill equals a subagent: you lose parallelism and context isolation.
- •MCP server fetched via
npxwithout a pinned version changing under you.
↩️ Rollback in one command
- ✓MCP:
codex mcp remove magnific(and metricool) brings the list back to empty. - ✓Wrappers:
rm ~/.local/bin/mcp-*.sh; the.envwas never touched. - ✓AGENTS.md:
mv ~/.codex/AGENTS.md.bak-AAAAMMDD ~/.codex/AGENTS.md. - ✓Roles:
rm -rf ~/.agents/skills/papel-*; the originals in~/.claude/agentsstay intact. - ✓If a key leaked, rollback isn't enough: rotate the key before anything else.
Key concepts
The subagent's prompt turned into an instruction the agent takes on.
Roles in sequence in the same context, not in separate contexts.
.agents/skills/Folder that Codex scans and sync-skills mirrors.
Documenting what you lose is worth more than pretending parity.
Self-check (optional): you want Codex to register the magnific MCP, which needs an API key stored in ~/projetos/openpcbotv2/.env. What's the right approach?
🎯 Project summary
set -a; source .env; set +a; the config stores a path, never a key.Next project:
3.3 — Curated memory: the agent proposes, you approve