Your agent config is already 90% portable. The gap that's left fits in one TOML header.
Moving from Claude Code to Codex to Cursor without rewriting your skills or MCP servers is roughly 90% solved by standards and a bit of generation. The one hard part, converting MCP to Codex TOML with secrets, rulesync does not cover. I read its source to be sure.
TL;DR
| What | Details |
|---|---|
| The thesis | Moving between coding agents is roughly 90% solved once you separate the portable (skills, MCP, instructions) from the rest. Hooks and sub-agents close further on Codex with a generator; Claude plugins stay the one true dead end. |
| The fan-out | rulesync propagates one source to 30-plus targets. It is the tool to know before writing your own generator. |
| The gap | Read in the v9.6.3 source: rulesync’s Codex MCP writer is a verbatim pass-through. It does not route secret HTTP headers into env_http_headers, it would copy the value in clear text. |
| The fix | A pure JSON-to-TOML function that routes ${VAR} by name and refuses mixed strings. One file. |
| The two scales | Monorepo project-scoped (versioned config) versus solo global multirepo (config in the home). Same engine, redirected output. |
A solo developer, call him Jérôme, works across many repos. POCs he never versions, no team, and the habit of hopping between agents through the day: Claude Code here, Codex there, Cursor Agent inside Conductor. His question fits in one line. How do I stop rewriting my skills and my MCP servers every time I switch tools?
The point almost everyone misses up front: his need is global at the machine level, not at the repo level. When your repos are throwaway POCs that never get committed, a per-project versioned config covers nothing, it exists nowhere. He needs a setup that lives in the home directory and that every repo inherits. That is the exact inverse of a monorepo, where one committed config settles the question for the whole team. We come back to it below, because those two worlds want the same engine pointed at two different places.
Scope, stated plainly: this piece covers three tools, not the thirty-plus rulesync targets. Claude Code, Codex and Cursor are what’s actually running inside Méthode Aristote today, not a curated sample. Nobody mandated one harness, each person picked their own, and these three are what people converged on. The generate-don’t-symlink principle applies the same way to Zed, Cline or Windsurf, untested here, not excluded on purpose. (Frédéric C. asked about the other 30-plus targets after reading a draft; the answer above is the honest one, not a dodge.)
Why not just standardize on one tool, the obvious question and the fair one, raised independently by Livio G. and Frédéric C. after a draft of this piece went out. Two answers, not mutually exclusive. The economic one: each CLI comes with its own plan and its own usage ceiling, splitting work across tools spreads the load across several quotas instead of paying full price for API access on one. The organizational one, more concrete: Méthode Aristote runs two senior engineers, three product profiles and two QA interns who all ship PRs, plus about ten non-technical ops and sales people, plus freelance tutors and salespeople who bring their own tooling already. Mandating one harness across that mix is a bigger ask than it looks from outside, and it holds regardless of whether the tooling gap in this piece is “real” or self-inflicted.
What carries over, and what doesn’t
The good news is that most of the work is already done by standards. Three bricks move from one tool to another with almost no effort, and only one resists.
Skills first. Since late 2025 they follow the Agent Skills Open Standard (agentskills.io), published by Anthropic and then picked up by a growing number of platforms. The format is a single SKILL.md file with a short frontmatter (name and description required, the rest optional). Each tool then scans its own discovery folders:
| Tool | Folders scanned |
|---|---|
| Claude Code | ~/.claude/skills/, .claude/skills/ (per repo). Symlinks supported. Does not scan ~/.agents/. |
| Codex CLI | ~/.codex/skills/, .codex/skills/, .agents/skills/, $HOME/.agents/skills/ |
| Cursor | ~/.cursor/skills/, .cursor/skills/, and also reads ~/.claude/skills/, ~/.codex/skills/ |
The practical consequence: dropping your skills into ~/.claude/skills/ and ~/.codex/skills/ covers all three tools, since Cursor reads both. One caveat worth checking again before you publish: Cursor’s cross-tool reads come from community docs more than a fixed spec, and the count of compatible platforms moves fast. The guide keeps the same discovery-folder logic in more detail, tool by tool: multi-directory skill discovery for cross-CLI compatibility.
Instructions next. Codex and Cursor read AGENTS.md natively. Claude Code goes its own way and does not read it. For Claude you need either an @AGENTS.md import inside CLAUDE.md, or a symlink CLAUDE.md -> AGENTS.md. One line of plumbing, nothing more.
One Codex-specific limit worth knowing before you lean on it: AGENTS.md is discovered per directory, nested from the project root down to the working directory (AGENTS.override.md wins over AGENTS.md in the same folder), and the combined text caps at 32 KiB by default (project_doc_max_bytes), truncated silently past that. Hit it firsthand at 31,009 bytes with no margin left; the fix was moving a section into a nested .codex/AGENTS.md, not trimming the root file’s substance.
One trap worth naming before someone tries it as a shortcut: ~/.codex/config.toml exposes project_doc_fallback_filenames, which looks like it would let you point Codex straight at an existing CLAUDE.md instead of maintaining a separate AGENTS.md. A GitHub issue on the point (openai/codex#22454) shows the failure mode, and the actual cause: the reporter had project_doc_fallback_filenames = ["CLAUDE.md"] nested under a [project] table, where Codex silently ignores it. An OpenAI maintainer identified the real requirement, the key has to sit at the top level of the TOML file, not inside [project], and the reporter confirmed it works once moved. Not a broken feature, a config-placement footgun with no error message to catch it.
Then MCP, and this is where it snags. The formats diverge, which rules out a plain symlink from a single source.
| Tool | Format | Location |
|---|---|---|
| Claude Code | JSON, key mcpServers | .mcp.json (project) or user scope via claude mcp add --scope user |
| Cursor | JSON, key mcpServers, same schema as Claude Desktop | ~/.cursor/mcp.json, .cursor/mcp.json |
| Codex CLI | TOML, [mcp_servers.x] | ~/.codex/config.toml |
Between Claude and Cursor, it is a direct copy, same JSON schema. The only place you actually have to convert is toward Codex: JSON to TOML. In Codex TOML, a stdio server carries command, args, env; an HTTP server carries url plus its headers. For a secret header, Codex exposes two keys: env_http_headers, a map from header to environment variable name, and bearer_token_env_var, the shortcut for the Authorization header. In both cases you reference the name of the variable, never its value. Each tool reads the value at runtime from the env. For concrete server configs beyond this JSON/TOML split, the guide’s MCP servers ecosystem page catalogs them.
And what does not carry over? It splits by tool, not as one block, and the article this originally shipped as got that wrong by bundling everything together.
Codex CLI has its own hook system, .codex/hooks.json, same UserPromptSubmit array shape as Claude’s settings.json: one entry per hook, type: "command", a timeout. It also has its own sub-agent format, .codex/agents/*.toml, an officially documented schema (learn.chatgpt.com/docs/agent-configuration/subagents). Neither is a Claude-only concept. Both convert with a generator, the same move the MCP section below makes for TOML, detailed after the figure.
Cursor is not the dead end this piece first claimed, but it is not a peer of Codex on this either. It has had its own hooks system since version 1.7 (.cursor/hooks.json, still labeled beta) and its own sub-agent format since 2.4 (.cursor/agents/*.md, YAML frontmatter). The concept exists. What the concrete trick below needs from it does not: additional_context, the field a hook uses to inject text into the conversation, is wired end-to-end only for the sessionStart event. A Cursor team member confirmed on the forum that for other events, postToolUse included, the field is accepted, logged, and then silently dropped, plumbing “started but not completed.” And beforeSubmitPrompt, the one event that matches Claude’s UserPromptSubmit, is documented for the desktop IDE but not confirmed to fire in the CLI at all. The same test that produced a hook-generated hint on Claude Code and Codex produced nothing on Cursor, and given the state of additional_context, that is closer to a known limitation than a missing feature.
Path-scoped rule loading runs the other way round. Cursor’s .cursor/rules/*.mdc frontmatter (globs, alwaysApply, description) is a close match for Claude’s per-path rules, arguably richer since it adds a fourth mode, an “agent decides from the description” rule Claude doesn’t have. Codex has nothing like it inside a single directory, only the nested AGENTS.md hierarchy described above, one file per folder, no glob.
The .claude-plugin/plugin.json manifest and the marketplace are the one holdout with no analog anywhere else, on any tool. Skills, MCP and instructions still cover the bulk of day-to-day parity, and that number is a gut estimate, not a measurement. Hooks and sub-agents are not the lost cause the rest of this piece used to claim, on Codex they take the same generator treatment as MCP, but they are not a solved problem everywhere either: Cursor has the concept, not yet the working plumbing for it.

(The left image above still shows the old two-column split, hooks/sub-agents/plugins bundled as one non-portable block. It needs a redraw to match the caption; flagging it rather than silently leaving a picture that contradicts its own text.)
Hooks and sub-agents: the fourth brick, not a dead end
On Méthode Aristote, the same generator that writes .codex/config.toml (sync-ai-instructions.ts) also wires Codex’s hooks and produces its sub-agent TOML. Tested live in three separate Codex CLI TUI sessions on three different machines, not just unit-tested against a schema.
.codex/hooks.json doesn’t duplicate the routing logic, it points Codex at the exact same file Claude Code runs: .claude/hooks/user-prompt-submit/bm25-suggest.js. Both harnesses call it with the same stdin/stdout contract, {"prompt": "..."} in, {"systemMessage": "...", "hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "additionalContext": "..."}} out. The systemMessage field didn’t exist in the hook’s original output, written for Claude’s additionalContext-only convention, and had to be added for anything to render in Codex’s TUI. Confirmed on a live build (0.146.0-alpha.9.2): the prompt “mon Prisma refuse de se connecter en staging” produced the same BM25 hint, /tech:diagnose at 61-62%, from the same corpus, in both a Claude Code session and a Codex session.
One piece of friction the generator cannot remove: Codex approves each hook by hash at its position in the array. Add a hook and every entry after it shifts index, which silently invalidates prior approvals; the developer has to reopen /hooks in the TUI and re-approve. Real cost, not zero-friction, but a re-approval step is a different claim than “no equivalent.”
Sub-agents follow the same move. Claude’s .claude/agents/*.md frontmatter (name, description, model, tools, category) converts to Codex’s TOML schema (name, description, developer_instructions required; model_reasoning_effort, sandbox_mode, mcp_servers optional). Claude’s model: haiku has no Codex equivalent to copy to, so the converter only translates the reasoning-effort axis and drops the model field outright rather than guess an OpenAI model that isn’t there, the same refuse-over-guess rule extractEnvVarName applies to secrets in the MCP section below, reused on a different ambiguous field. One example, generated straight from a Claude agent source with model: haiku:
name = "aristote-context"
description = """
Use at the start of any task requiring project awareness [...]
"""
model_reasoning_effort = "low"
sandbox_mode = "read-only"
developer_instructions = """
Tu es un agent de contexte léger [...]
"""
Seventeen agents converted this way, all syntactically valid TOML, checked with Python’s tomllib. What’s confirmed stops at discovery: Codex lists the .toml files and reads name/description/model back correctly when asked to. Actual invocation, Codex picking one of these up and dispatching a real task to it, is untested. That gap stays open rather than papered over as proof of full parity.
Generate, don’t symlink
Since the MCP format differs between JSON and TOML, a symlink from a single source is not enough. You need a generator. Two paths exist, and the first is the default one.
rulesync (npm, dyoshikawa/rulesync) fans one source out to 30-plus targets: claudecode, codexcli, cursor, copilot, goose, zed, warp, cline, roo, devin and more. It handles rules, ignore files, MCP, commands, sub-agents and skills. Instead of coding your own generator, you describe a source and rulesync produces each config. For 90% of cases the answer stops there: install rulesync, describe your source, regenerate. It is the maintained tool, broad, with the longest target list on the market.
One public repo shows the pattern in the wild: github.com/LivioGama/agent-config, the author’s dotfiles, leans on rulesync for the fan-out and syncs its skills with rsync. The snippet is interesting because it dodges a trap:
# t ∈ codex cursor gemini devin claude
rsync -aL "${EXCLUDES[@]}" "$HOME/.agent-config/skills"/ "$HOME/.$t/skills"/
The -L resolves symlinks into real files, the sync is additive, each tool keeps its own skills. Why a copy and not a symlink? Because the Cursor CLI has a reported bug (forum.cursor.com, thread 163569 plus a cluster of tickets around it): it only discovers symlinked skills when the link target sits under .cursor/ or .claude/. A target elsewhere is ignored. The status is disputed. A Cursor team member (Dean Rie) reported discovering all eight skills on his own build regardless of symlink location, and offered two explanations: either it was already fixed by 2026.06.15, or his test set differed from the reporter’s. The reporter pushed back with screenshots comparing the Desktop app against the CLI on that same build, fewer skills visible in the terminal than in the app. It does not matter who is right: rsync -aL produces real files and works around the problem whatever its status. The safe recommendation stays the copy, not the symlink.
That same repo shows rulesync’s limit, but only on the one source that runs through it. Its .rulesync/mcp.json is thin: a single server, hardcoded absolute path, JSON-to-TOML conversion for that source delegated to rulesync outright. The repo also runs a second, separate pipeline for the rest of its MCP fleet, a hand-rolled Python converter (sync-mcp-servers.sh), and its own Codex TOML writer handles secret headers worse than rulesync’s pass-through: no ${VAR} extraction at all, header values land in the TOML as written unless the source JSON already spells out bearer_token_env_var by hand. The hard point does get stress-tested in that repo, just not by rulesync. I wanted to know how far rulesync itself holds on Codex MCP, so I went to the source.
What rulesync actually generates for Codex, read in the source
I had a doubt about how deep rulesync’s Codex MCP support really goes, so I went to read the source at tag v9.6.3: src/features/mcp/codexcli-mcp.ts and src/types/mcp.ts. The npm tarball only ships a minified bundle, a grep over it gives false positives, only the source settles it.
The writer, convertToCodexFormat, treats field values it doesn’t recognize as a pass-through. It only maps five things by name: envVars becomes env_vars, enabledTools becomes enabled_tools, disabledTools becomes disabled_tools, disabled: true becomes enabled = false, and oauth.clientId gains an oauth.client_id. Every other key, including headers, falls into the else branch and is copied verbatim. Real machinery sits around that pass-through: server names get normalized and checked against a prototype-pollution denylist, and a separate cleanup pass strips null values and empty nested tables before the TOML is written. None of that machinery inspects what a headers value actually contains. The input schema McpServerSchema is a z.looseObject, so unknown keys survive instead of being stripped.
What it does not do, said precisely. rulesync does not generate, from a unified source, the Codex timeouts (startup_timeout_sec, tool_timeout_sec), nor the default approval mode (default_tools_approval_mode) nor the per-tool approval overrides (approval_mode), nor the secret HTTP headers (env_http_headers, bearer_token_env_var). None of those strings exist in the writer or the schema. The single occurrence of approval_mode in the whole file is a comment documenting a round-trip preservation: the [mcp_servers.<server>.tools.<tool>] table that the Codex CLI writes itself when the user approves a tool (issue #1709), re-merged so a regenerate does not wipe the approvals. Nothing here counts as an emission from a source.
One honest nuance. Because the schema is a looseObject and the writer is a pass-through, a developer who already wrote those keys in exact Codex snake_case in their JSON source would see them copied straight through. But with no transformation, no validation, and above all no reasoning about secrets. That’s the absence of blocking, not support.
The gap that justifies writing your own converter is the secret HTTP header. env_http_headers appears nowhere in rulesync’s code. Yet its schema accepts a headers key, a name-to-value map, which lands in the pass-through. Concrete translation: if your HTTP server carries a headers block with an API key, rulesync copies the literal value in clear text into the generated config.toml. No routing to env_http_headers, no reference by variable name. The secret value ends up written to disk. That is exactly the behavior a config generator must never have.
The home-made converter that fills the gap
On Méthode Aristote, the MCP conversion goes through scripts/ai/codex-mcp.ts. The engine is a deterministic function, buildCodexTomlConfig, that takes the server map and returns the text of the [mcp_servers.*] block. No env reads, no disk access, same inputs always produce the same bytes. Its one observable effect is a console.warn when it rejects a mixed placeholder, which matters for a diagnostic log, not for the determinism that makes it unit-testable and replayable in the pnpm ai:mcp:check check.
The shared source is a YAML file, mcp-servers.yaml, opening with version: 1 and a top-level servers: map. Each entry carries its base definition and, optionally, a codex: block for Codex-specific settings. An excerpt for the HTTP server with a secret header, context7:
servers:
context7:
type: http
url: "https://mcp.context7.com/mcp"
headers:
CONTEXT7_API_KEY: "${CONTEXT7_API_KEY}"
codex:
startupTimeoutSec: 15
The generated Codex TOML:
[mcp_servers.context7]
url = "https://mcp.context7.com/mcp"
env_http_headers = { "CONTEXT7_API_KEY" = "CONTEXT7_API_KEY" }
startup_timeout_sec = 15
Look at the env_http_headers line. To the right of the equals sign, the name of the variable, not its value. The whole difference with rulesync’s pass-through sits right there. The split is driven by the shape of the value. extractEnvVarName only recognizes a string that is strictly ${VAR}, via the regex ^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$. It then becomes a reference: env_vars = ["VAR"] for a stdio server, a header = variable_name pair inside env_http_headers for an HTTP one. A static value with no ${...} goes through as-is into the env sub-table or an inline http_headers. And a mixed string like Bearer ${TOKEN} is refused with a warning, never written, so half a secret never lands in the generated file.

${VAR} reference goes through env_http_headers by name, a mixed string like Bearer ${TOKEN} is rejected. rulesync would copy the value as-is.The rest of the Codex settings are declared in the codex: block of the YAML and translate one to one:
YAML (codex:) | Generated TOML | Effect |
|---|---|---|
enabled: false | enabled = false | keeps the definition, one-line reactivation |
startupTimeoutSec: 15 | startup_timeout_sec = 15 | startup timeout |
enabledTools: [query] | enabled_tools = ["query"] | allowlist of exposed tools |
defaultToolsApprovalMode: approve | default_tools_approval_mode = "approve" | default approval mode |
tools.<t>.approvalMode: approve | [mcp_servers.<n>.tools.<t>] + approval_mode = "approve" | per-tool approval |
A stdio server config comes out with its args resolved and its env referenced, placeholder {{PROJECT_DIR}} replaced by . for Codex. Serena ran through this exact pipeline before being retired from the live server list, kept here because it is the clearest real trace of the resolution mechanism:
[mcp_servers.serena]
command = "uvx"
args = ["--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server", "--context", "ide-assistant", "--project", ".", "--open-web-dashboard", "false"]
cwd = "."
[mcp_servers.serena.env]
CLAUDE_PROJECT_DIR = "."
What is not handled, said plainly too. The builder never emits bearer_token_env_var: an HTTP secret always goes through env_http_headers. It emits no required field. And the oauth: blocks in the YAML are ignored by the Codex target, so an OAuth server like slack is carried with codex.enabled: false rather than half-translated. The scope stays small, and every part of it is something Aristote controls directly.
Compared to rulesync, breadth is not the point. rulesync wins on targets outright. The point is the exact surface covered on the one target that matters here. buildCodexTomlConfig knows how to produce the timeouts, the allowlist, the default and per-tool approval, the commented enabled = false, and most importantly the secret split by reference with mixed-string refusal. On the precise question of secret HTTP headers, feature count is not the axis that matters. A file that never writes a secret in clear text is the one worth shipping.
The obvious objection
“Why not just take rulesync and stop there? Thirty targets, a maintained tool, and you are pitching a home-made function for one edge case.”
That is the best pushback. And for most people, the answer is yes, take rulesync and stop there. The 90% is well worth the zero cost of a tool that is already written.
But the objection holds only as long as your HTTP MCP servers have no secret in their headers. That has become the common case: Context7, Perplexity, most hosted HTTP servers want an API key passed as a header. The day you add one, rulesync puts you in front of a choice it does not name. Either you write the literal value in your source JSON and it ends up in clear text in the generated TOML, or you hand-code the env_http_headers key in Codex snake_case straight into your source, which breaks the very principle of a single tool-agnostic source. Both are bad. The home-made function is the third choice: you write ${CONTEXT7_API_KEY} once, and the converter routes by name. Small scope, but it hits the one place where the pass-through does real damage.
Two scales for the same engine
The Aristote system applies all of this to a monorepo, project-scoped, versioned, the same pattern the guide documents generically at 3.5 Team Configuration at Scale: YAML profiles, composable modules, a skeleton, a sync script, gitignored regenerated output. The source lives under doc/guides/ai-instructions/: the per-tool skeletons, the common core, the modules, the canonical rules, the profiles, and mcp-servers.yaml. The generator sync-ai-instructions.ts delegates the TOML conversion to codex-mcp.ts. You never edit the outputs, you edit the sources and rerun pnpm ai:sync. The outputs (CLAUDE.md, .cursorrules, AGENTS.md, and the three MCP configs) are gitignored, regenerated on every checkout according to the developer’s profile.
The profiles drive the generation. The file profiles/florian.yaml lists tools: [claude-code, codex], a direct_brutal tone and a handful of modules: its sync therefore writes .mcp.json and .codex/config.toml, but not .cursor/mcp.json. Three guardrails wrap the whole thing. Locally, pnpm ai:mcp:check regenerates the Codex TOML in memory and compares it byte for byte against .codex/config.toml, scans for residual ${...} and suspiciously long values, then checks that the env variables are present without ever printing their value. In CI, .github/workflows/ai-config-check.yml replays the generation and a drift check. And a Husky post-merge hook reruns the sync after a pull, only if the sources moved, and never blocks.
Florian’s own profile skips Cursor, and the reason traces the team’s actual adoption curve. Non-technical profiles at Méthode Aristote started on Cursor, since its IDE-first interface demanded less CLI comfort than Claude Code or Codex early on. Some later moved to Codex once its CLI got approachable enough; Claude Code still expects more comfort with a real command line. Read today, Codex looks like the better entry point for a non-technical contributor who wants to start shipping code, closer to an IDE experience than Claude Code, without a separate license and account to manage like Cursor. The likely direction inside Aristote, not decided yet: drop Cursor, consolidate on Codex and Claude Code.
Here is the central contrast. This system never touches the home directory. It writes into the repo paths, .mcp.json, .codex/config.toml, relative to the root. The only time it looks at ~/.cursor/mcp.json, it is read-only, to warn that it would duplicate the repo config. For Jérôme’s need, the solo global multirepo, you would redirect the output-path functions to homedir(), write into ~/.codex/config.toml, ~/.cursor/mcp.json, ~/.claude, and on the Claude side prefer claude mcp add --scope user over a committed .mcp.json. The conversion engine itself does not change by one line. It is the same buildCodexTomlConfig, the same secret logic. You just move the write target from the repo to the home.

Conductor slots into the solo case nicely, by the way, since it drives all three harnesses and inherits the user config per worktree. With one limit to know: gitignored files (.env, personal config) do not follow on their own into the worktrees, which forced the addition of .worktreeinclude and setup scripts. “Inherited per worktree” only holds for what is tracked.
The second brick: fewer CLI round trips
So far the whole topic is config sync. There is another agent-tooling lever, distinct but in the same economy logic: cutting the round trips and the tokens on the CLI operations an agent repeats in a loop.
usable-git illustrates it on the Git side. The project exposes Git as a semantic API for agents, ten operations in the current release candidate (inspect, review, history, diff, publish, push, ship, branch, sync, update) instead of chaining git status, git add, git commit, git rev-parse. An earlier prototype reported a headline number, 21x faster, 18.94 ms versus 0.89 ms, for a direct-object-write path the current release candidate no longer uses. The maintainer now labels that result plainly: historical and unverified in the current checkout, raw artifacts missing, not a v1 performance claim. What survives the retraction is the shape of the argument, not the number. The common commit-and-push flow is two calls, inspect then ship, instead of three, the response envelope is compact, and the result is structured data instead of stdout to parse. Fewer steps where the agent can go wrong, fewer tokens burned. Cite the mechanism, not the retired benchmark.
The link with RTK, a Rust proxy that filters noisy CLI output before it reaches the model, is direct: same thesis, cut tokens and reasoning steps on CLI operations, applied this time to Git via a semantic API rather than a command proxy. The two bricks answer each other. Less config to rewrite on one side, fewer calls and less parsing on the other. The caveat on usable-git matches its own status: a release candidate with decision-complete specs but no tagged release yet. Borrow the pattern now, evaluate the package once it ships.
What is genuinely still open
The honest summary: portability between agents is a sorting job before it is ever a build project. Separate the portable from the tool-specific, generate instead of symlink, and 90% of the problem drops. rulesync does the bulk of the fan-out. The one technical gap left, converting MCP to Codex TOML with secrets and timeouts, closes with a deterministic function in a single file, the same one you point at the repo or at the home depending on whether you run a monorepo or solo.
One place this used to break sits on the Claude side, not the conversion, and it is worth closing precisely because the record online is stale. Two tickets, #16728 and #32939, reported that claude mcp add --scope user wrote into projects["<path>"].mcpServers in ~/.claude.json instead of a flat cross-project store, on Claude Code 2.1.1 and 2.1.72. Both were closed not_planned, neither with a maintainer confirming the report. I checked directly on the version running while this piece was written, 2.1.221: user-scope servers on this machine sit in a genuine top-level mcpServers key in ~/.claude.json, separate from the per-project projects[<path>].mcpServers entries, and claude mcp list surfaces them from a project that never added them. That matches the current docs, which the guide’s MCP configuration location section also reflects, and contradicts both tickets. Whatever broke it in January and March 2026 appears fixed in a later release, quietly enough that neither ticket carries a closing comment saying so. If you are running an older build, check your own ~/.claude.json before trusting either the docs or this paragraph, that ten-minute check has more evidence value than either source.
One honest caveat that applies to the whole piece, not one section, and Frédéric C.’s read after publication nails it: this is a snapshot dated early August 2026, not a reference doc. Every version cited here, Claude Code 2.1.221, rulesync v9.6.3, Codex 0.146.0-alpha.9.2, will move, and the rules and workarounds described will likely be partly obsolete within three to six months as the underlying tickets get fixed or the tools change shape. The two-minute fix for that: check your own installed version before trusting a paragraph here over what you observe directly, the same move already applied above to the Claude ticket.
YSNK
(You should now know)
- Hooks and sub-agents are not Claude-only: Codex has its own
.codex/hooks.json(sameUserPromptSubmitJSON contract) and its own.codex/agents/*.tomlschema, both generatable from Claude’s sources and confirmed working in live Codex TUI sessions. Cursor has the same concepts since versions 1.7 and 2.4, but its hook context-injection is confirmed broken everywhere except one event,sessionStart - Cursor also reads Claude’s and Codex’s skill folders natively, so dropping skills into
~/.claude/skills/and~/.codex/skills/covers all three tools without a Cursor-specific folder - The Cursor symlink bug is disputed (a Cursor team member reports finding all eight skills on his build, the reporter counters with screenshots showing fewer skills in the CLI than in the Desktop app on the same version), but
rsync -aLsidesteps the argument entirely by writing real files regardless of which side is right pnpm ai:mcp:checkregenerates the Codex TOML in memory, diffs it byte-for-byte against the committed file, scans for residual${...}placeholders, and confirms env vars are present without ever printing their valuesusable-git’s headline “21x faster” number (18.94ms to 0.89ms) is now labeled by its own maintainer as historical and unverified, tied to a code path the current release candidate no longer uses. What survives is fewer agent-facing calls, not the retired numberclaude mcp add --scope userreportedly collapsed to a single project’s path on two 2026 builds, both tickets closednot_plannedand unconfirmed. Tested live on the current version, it sits in a genuine flat top-level key, cross-project, matching the docs
Related
Non-technical to production in 10 days with Cursor AI
A non-developer modified 80 files in production in 10 days with Cursor AI. Exact timeline, AI config setup, and what it means for engineering teams in 2026.
1/6 · The same model, opposite results: context is the variable
Same model, same stack, opposite outcomes: +67% vs -19%. The variable isn't the AI. It's context engineering, and Princeton research explains exactly why.
Claude is my second contributor: what real Git stats show
6 contributors in our git history. One is an AI. What the commit patterns actually look like after months of Claude Code, beyond the marketing claims.