PROJECT 3.2

🔌 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.

6
Topics
~45
Minutes
Intermediate
Level
Project
Type

🎯 The project on one screen

GoalCodex gets the same external tools as Claude (MCP), and the behavior Claude got from hooks now exists in Codex as read text and role skills.
You leave withMCP servers registered in Codex with no secret copied, the 15 adapter skills cleared for porting, a "pacing rule" section in ~/.codex/AGENTS.md and 7 role skills in .agents/skills/.
Acceptance criteriacodex 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.
1

🧾 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.

MCP · tools TRAVEL Claude Code 2 global + 2 per project Codex CLI today: none MCP server magnific · metricool the same process serves both: it's a protocol HOOKS · events DON'T TRAVEL Claude harness SessionStart ×2 context-mode fable-mindset Codex harness PostToolUse Stop no SessionStart there is no bridge between harnesses way out: the event becomes TEXT in AGENTS.md read at session start, by instruction

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

ScopeClaude CodeCodex CLI
Global MCPmagnific, metricoolnone
Per-project MCPwifi → klingai, 2cerebrox → cerebro-vipnone
Hooks2 SessionStart (context-mode, fable-mindset)PostToolUse + Stop (impeccable)
Subagents7 in ~/.claude/agentsno equivalent
Skills blocked by this15 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

MCP

An external tool spoken to over a protocol; any runtime that speaks the protocol can use it.

Hook

An event hook of the harness; it's not a protocol, it's a local implementation.

Global vs project scope

An MCP can apply to the whole machine or only inside one repo.

Baseline

The state measured before the change; without it there is no "it got better".

2

🔑 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 +aset -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.

1

Find the real command

In Claude, each MCP already has a startup command. Copy the command, not the key.

2

Write the wrapper

One .sh per server in ~/.local/bin/, with set -a; source ...; set +a and exec at the end.

3

Register it pointing to the wrapper

codex mcp add magnific -- ~/.local/bin/mcp-magnific.sh. The configuration stores a path, not a secret.

4

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 source on the .env at 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 700 on the wrapper and .env kept 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 .env into ~/.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

Startup wrapper

A short script that loads the environment and runs exec on the real server.

set -a

Makes source export everything; set +a switches it off right after.

Reference vs copy

Point to the source of the secret; never create a second source.

Negative proof

The acceptance test is a grep that finds nothing — verified absence.

3

🔓 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.

avatar-heygen-nei heygen-cli heygen-mcp espiona-ads ugc-seedance25 website-intelligence comfy-relay printing-press (8 variants)

✓ Ready to port now

  • Only reads and writes files, or calls binaries already on the machine.
  • The SKILL.md doesn't mention any mcp__… 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.md says "call mcp__magnific__images_generate".
  • Depends on a Claude plugin (superpowers, context-mode, claude-mem).
  • Depends on a subagent: "dispatch to analista-neutro".
  • Assumes AskUserQuestion or 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

Adapter skill

A skill whose value depends on a registered external tool.

Failing halfway

Worse than not having it: the skill is picked and breaks after the promise.

Tool before skill

The order that avoids installing something broken in Codex.

Block by dependency

8 printing-press variants unlock with a single MCP.

4

🪝 Hooks: the real mapping between the two

Here the news is mixed. Claude has two SessionStart hookscontext-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.

session time SessionStart Claude: context-mode + fable-mindset Codex: ✗ doesn't exist PostToolUse matcher Edit|Write|apply_patch Codex: ✓ exists (5s) Stop impeccable · design deep pass Codex: ✓ exists (30s) portable substitute section in AGENTS.md + prime skill read at the start already equivalent: the same impeccable hook.mjs in both nothing to migrate here — just check the timeouts

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

EventClaudeCodexWhat to do
SessionStart✓ 2 hooks✗ doesn't existTurn it into text in AGENTS.md + a prime skill
PostToolUse✓ impeccableNothing; check the matcher
Stop✓ impeccableNothing; 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

SessionStart

Opening hook; exists only in Claude, and it's the one that hurts most to lose.

Matcher

Filter for which tools trigger PostToolUse.

Silent failure

A hook written not to break the session when its target disappears.

Event → reading

The only possible translation when the hook doesn't exist on the other side.

5

📝 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

Native skill

Depends on a harness feature; only the content migrates.

Pacing rule

Think before you act; close the loop by verifying.

Guarantee vs request

A hook always runs; text depends on being read and obeyed.

Short wins

A long instruction isn't stronger, it's more ignored.

6

🎭 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 list shows magnific and metricool. Evidence: command output.
  • grep -rIl -e 'sk-' -e 'API_KEY=' ~/.codex returns 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.md and is cited in a codex 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 list and the SessionStart hooks remain the same. Evidence: comparison with the baseline from topic 1.

⚠️ Risks of this project

  • A key copied into ~/.codex in 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 npx without 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 .env was never touched.
  • AGENTS.md: mv ~/.codex/AGENTS.md.bak-AAAAMMDD ~/.codex/AGENTS.md.
  • Roles: rm -rf ~/.agents/skills/papel-*; the originals in ~/.claude/agents stay intact.
  • If a key leaked, rollback isn't enough: rotate the key before anything else.

Key concepts

Role skill

The subagent's prompt turned into an instruction the agent takes on.

Loss of parallelism

Roles in sequence in the same context, not in separate contexts.

.agents/skills/

Folder that Codex scans and sync-skills mirrors.

Honest equivalence

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

Tools travel — MCP is a protocol: magnific, metricool, klingai and cerebro-vip now exist in Codex too.
The secret stays at the source — a wrapper with set -a; source .env; set +a; the config stores a path, never a key.
Events don't travel — PostToolUse and Stop already exist in both; SessionStart doesn't exist in Codex and becomes text that gets read.
Behavior becomes content — fable-mindset becomes the pacing rule in AGENTS.md; the 7 subagents become role skills.

Next project:

3.3 — Curated memory: the agent proposes, you approve