Portable agent configuration is a release system, not a shared folder
A release model for instructions, skills, Output Styles, hooks, MCP definitions and BM25 routing without confusing installed files with working behavior.
Jump to summaryWritten by Florian Bruniaux
AI Founding Engineer at MΓ©thode Aristote, 13 years scaling engineering teams from developer to CTO. Builds open-source developer tools, see what else I've shipped.
TL;DR
| Layer | Portable unit | What still varies |
|---|---|---|
| Source | Shared policies, skill bodies, response styles, scenarios and server definitions | Host overlays and declared exceptions |
| Build | One immutable release with native Claude Code and Codex projections | Filenames, schemas and adapters |
| Install | Digest-bound changes to live instructions, skills, styles and hooks | Native paths, trust stores and lifecycle |
| Runtime | The same intended policy and routing corpus | What each client loaded, invoked and enforced |
| Audit | One static evidence contract | Behavioral canaries for each surface |
I used to describe agent configuration with a fixed portability percentage and MCP conversion as the last gap. That framing was convenient and wrong. I had not measured the percentage, and a secret header is only one conversion problem inside a larger delivery system.
The configuration I now use for Claude Code and Codex has six separate concerns: canonical source, immutable releases, host projections, transactional installation, mutable routing state and runtime evidence. Some files can be shared byte for byte. Other parts need a host adapter. None of those structural choices proves that a running client followed the result.
This article is the stable map of that system. It centers Claude Code and Codex because those are the two hosts I implemented and audited. Cursor appears only as an adjacent example where a common concept may exist with a different contract. I do not infer parity from similar filenames.
The portable unit is a source tree
Copying one folder into two home directories removes some duplication. It does not create a reliable configuration system. A dependable setup must answer four questions:
- Which files are authoritative?
- Which artifacts belong to each host?
- Which exact release is live?
- What evidence shows that the client used it?
I keep the editable authority in one source tree. Shared rules sit beside short Claude Code and Codex overlays. Skills have one canonical body. Hook policies live in pure cores with separate host adapters. Large references remain outside the always-loaded instructions.
agent-config/
βββ src/
β βββ instructions/
β β βββ shared.md
β β βββ claude.md
β β βββ codex.md
β βββ skills/
β β βββ common/
β β βββ host-specific/
β βββ hooks/
β β βββ policy-core.mjs
β β βββ claude-adapter.mjs
β β βββ codex-adapter.mjs
β βββ output-styles/
β β βββ claude.md
β βββ routing/
β β βββ scenarios/
β βββ reference/
βββ releases/
βββ scripts/
The renderer combines the shared instructions with one host overlay. It writes a flat CLAUDE.md for Claude Code and a flat AGENTS.md for Codex. I do not make either live file import the mutable source tree. A session should see one self-contained projection from one identified release.
That distinction also resolves the project versus global question. The build model stays the same. Only the installation target changes.

The src/ tree is canonical source. The releases/<release-id>/ tree contains inert build projections. Neither is a live discovery location. Installation places selected artifacts in these native targets:
| Surface | Claude Code project | Claude Code global | Codex project | Codex global |
|---|---|---|---|---|
| Instructions | CLAUDE.md | ~/.claude/CLAUDE.md | AGENTS.md | ~/.codex/AGENTS.md |
| Skills | .claude/skills | ~/.claude/skills | .agents/skills | ~/.agents/skills |
| Output style | .claude/output-styles | ~/.claude/output-styles | N/A | N/A |
| Hooks | .claude/settings.json | ~/.claude/settings.json | .codex/hooks.json | ~/.codex/hooks.json |
Project paths are relative to the repository root. Global paths are live user configuration. The installer compares those live targets with the selected release projections, but it never treats the targets as canonical source.
In this design, .codex/skills is a legacy compatibility input, not the canonical Codex skill root. The deployed router adds it automatically whenever the directory exists, with lower precedence and separate provenance from .agents/skills.
Project configuration and global configuration have different trust boundaries. A repository can review its local skills with the code. A global release affects future sessions across repositories. The global installer must not absorb a projectβs files, and a project sync must not rewrite the userβs home configuration.
Output Styles make the host-exception rule concrete. Claude Code has a native Output Style surface that modifies the main conversationβs system prompt. Codex has no equivalent artifact in this design, so the release contains only a Claude projection and records Codex as N/A. The Flow Lean Output Style case study shows why response defaults, project context, and full skills remain separate sources.
Build an immutable release before touching live state
Each build produces host-native artifacts under a content-addressed release directory:
releases/<release-id>/
βββ claude/CLAUDE.md
βββ claude/output-styles/flow-lean.md
βββ codex/AGENTS.md
βββ reference/ANTI_AI.md
βββ skills/
β βββ common/
β βββ codex-only/
β βββ projections/
β βββ claude/
β βββ codex/
βββ agents/
β βββ claude/
β βββ codex/
βββ hooks/
β βββ skill-router/
βββ artifact-manifest.json
The manifest records the hash of every artifact, the schema version and the runtime requirements. The release identifier derives from that canonical manifest. Rebuilding the same source should produce the same bytes. If it does not, the renderer stops.
Symlinks can reduce duplication inside the release, provided every resolved target remains inside that immutable tree. A link from a live skill directory back to a mutable working copy has different semantics. Its content can change without a new release identifier, which removes the evidence the release model was meant to provide.
The active current pointer identifies one complete release. It must resolve to a contained release directory, not an arbitrary path elsewhere on disk. Before activation, an independent validator recomputes the manifest and artifact hashes. Trusting the renderer to verify its own output would turn one bug into two matching claims.
The renderer is inert. It writes under releases/ and never mutates live Claude Code or Codex configuration. That boundary lets continuous integration build and inspect a candidate without permission to rewrite a developer workstation.
Install with an approval bound to the candidate and the live preimage
Global configuration has a large blast radius. One careless copy can replace the instructions, skills or hooks used by every new session. I separate preparation from installation.
The preparation step inventories the current live targets, hashes their exact state, compares them with the candidate and produces a redacted diff. It then calculates an approval digest that covers both sides of the proposed transaction:
approval digest = hash(
action
+ immutable release
+ candidate hashes
+ exact live preimage hashes
)
The apply step accepts one exact approval string. It verifies the release again, acquires an installation lock and rechecks every live preimage before the first write. A changed byte invalidates the approval. The installer prepares snapshots and a durable journal before it mutates anything, then uses atomic replacements where the filesystem permits them.
Rollback follows the same ownership rule. Before restoring a snapshot, the installer compares the current target with the postimage it recorded after installation. If another process or person changed the target, the result is UNKNOWN. The installer refuses to overwrite bytes it cannot prove it owns.
This process may look heavy for Markdown files. The consequence of those files is not small. They can steer shell commands, tool permissions, skill selection and hook behavior across every repository a developer opens.

Shared skills need native discovery and provenance
A skill body can be portable while its discovery path is not. The release projects one reviewed SKILL.md package into each hostβs native tree. Its build catalog records the normalized name, source host and source hash. The mutable router cache separately records each active skill path, root, scope and precedence, plus aggregate source and stat fingerprints for the selected host and project scope.
The release builder rejects two outputs that normalize to the same name and rejects source paths that escape an authorized root. Before that output check, its explicit host precedence chooses one source package and records catalog collisions. Runtime discovery follows its own precedence order: it keeps the first active skill and records each lower-precedence occurrence as shadowed_skill. A duplicate is therefore visible, but it does not make every occurrence disappear.
Native discovery remains the fallback. A router can improve recall, but it must not become the only way to access a skill. If routing evidence is weak, the skill stays installed and is marked native-only.
This is also the supply-chain boundary. A skill contains instructions that can influence tool use. Importing a folder from another repository without recording its source and hash distributes an unreviewed prompt to both hosts. The portable format does not make the content trustworthy.
BM25 routes hints, not authority
The routing layer addresses a different problem from file sharing. Once both hosts can discover a large catalog, including every full skill description in every prompt wastes context and increases accidental matches. I use a conservative BM25 index to suggest a small set of candidates.
The complete lifecycle is:
native skill discovery
-> provenance catalog
-> positive and contrastive negative scenarios
-> BM25 index and calibrated thresholds
-> cache for one host and one project scope
-> UserPromptSubmit hook
-> zero to three suggestions
-> host decides whether to invoke a skill
Each routed skill has positive scenarios and nearby negative scenarios. βReview this migration planβ and βimplement this migration planβ share most of their vocabulary, so positive examples alone produce an overconfident router. The negative set defines the nearest intent boundary.
The index uses a positive inverse-document-frequency floor and keeps the best scenario score per skill. It does not reward a skill merely because its author supplied more examples. Build-time evaluation uses leave-one-out probes from the scenario corpus. The current probe is removed from candidate documents, while BM25 inverse-document-frequency statistics still use the complete corpus. This is an internal regression check, not an independent held-out set. Skills that miss recall, trigger on unrelated prompts or lack enough contrastive evidence remain native-only.
The cache is mutable because it depends on the selected host, repository, working directory and discovered roots. It does not belong inside the immutable release. I key it by host and scope, record the release and catalog inputs that produced it, and rebuild it when those inputs change.
At prompt time, uncertainty returns no hint. An unavailable or malformed cache also returns no hint. The hook fails open because routing is an optional convenience, unlike a preventive policy hook that may need to stop an ambiguous write.
Two more boundaries keep the router in its place:
- An explicit skill invocation bypasses BM25.
- One prompt receives at most three suggestions.
The hook returns names and paths as context. It does not execute a skill. Claude Code or Codex still applies its native invocation rules, and a user can ignore the suggestion.
Hooks share policy, not payload parsing
Hook portability is often described as a question of whether both products support the same event name. That is insufficient. A preventive hook must reconstruct the complete candidate state from the hostβs tool payload before it can evaluate a policy.
I share the pure policy core and write one adapter per host. A Claude Code adapter can reconstruct a structured Write or Edit. A Codex adapter can parse an apply_patch operation and calculate every affected fileβs candidate content. Scanning raw patch text is not equivalent: removed text can contain a forbidden phrase, and separate hunks can create a violation only after application.
When an adapter cannot reproduce a supported write exactly, a preventive check returns UNKNOWN and denies the operation. Unsupported tools pass through because the hook does not claim coverage for opaque shell redirection, external editors or every process that can write a file.
Hook installation also remains host-specific. Each client needs its native declaration, timeout and trust flow. Changing a declaration can invalidate trust even when the adapter file has not changed. A fresh session is required for activation evidence because a long-lived client may retain the configuration it loaded at startup.
One MCP conversion bug shows why projections need policy
MCP is a useful case study, but it is not the whole portability problem. Claude Code accepts JSON server definitions, while Codex stores MCP configuration in TOML. A generator must translate schemas and preserve the security meaning of secret references.
I inspected the Codex MCP writer and MCP schema at the tagged v9.6.3 release. In that version, the writer maps a small set of known fields and passes other keys through. The input schema also preserves unknown keys. That makes broad conversion convenient, but the writer does not interpret a generic HTTP headers value as a secret reference that should become Codex env_http_headers.
The relevant source inspection is version-bound. It describes v9.6.3, not every later release and not every target supported by rulesync. Rulesync remains useful for broad fan-out. The case only shows why one host projection sometimes needs a narrower security policy.
My source format writes an environment placeholder, never the secret value:
servers:
documentation:
type: http
url: "https://mcp.example.test/mcp"
headers:
API_KEY: "${DOCUMENTATION_API_KEY}"
The Codex projection contains the variable name:
[mcp_servers.documentation]
url = "https://mcp.example.test/mcp"
env_http_headers = { "API_KEY" = "DOCUMENTATION_API_KEY" }
The converter accepts a placeholder only when the complete value matches ${VAR}. It rejects a mixed value such as Bearer ${TOKEN} instead of guessing how the target client will interpret it. Static values can still be emitted as static headers when that is an intentional, reviewed choice.
This conversion is deterministic and unit-testable. It still does not prove that the client process received the named environment variable, connected to the server or completed an MCP request. Those are runtime questions.
Cursor has related skills, instructions, hooks and MCP concepts, but I treat it as a separate projection until its discovery, payload and trust contracts have their own tests. Similar JSON is evidence of schema proximity, not operational parity.
Six evidence states prevent false confidence
Configuration audits become misleading when they collapse every layer into βworking.β I use six states for a capability such as a hook or shared skill:
| State | Example evidence |
|---|---|
| Present | A file or declaration exists at the expected path |
| Installed | Its bytes match the selected release manifest |
| Trusted | The client accepted the exact hook or extension identity where required |
| Loaded | A fresh client session reports or exposes the expected registration |
| Invoked | A correlated canary shows that the component ran |
| Effective | The observed file, decision or suggestion matches the intended result |
A matching hash can prove installation. It cannot prove loading. A log line can prove invocation while the client ignores malformed output. A suggested skill name can prove one positive route while negative and explicit-bypass cases still fail.

The static auditor therefore owns one machine-readable contract. It inventories selected Claude Code and Codex scopes, validates native instructions, skills, rules, agents, hooks, MCP configuration and release parity, then emits PASS, WARN, FAIL, UNKNOWN or N/A findings. An evaluator consumes that report instead of rescanning the same files with a second policy engine.
Runtime evidence is attached separately. Useful canaries include a prompt that should suggest one skill, a nearby prompt that should not, an explicit invocation that must bypass ranking, a preventive hook write that must be denied and an MCP placeholder that must resolve without printing its value.
An Output Style needs one additional configuration observation: which style the resolved settings selected. Selection still does not prove loading or effect. A fresh-session response canary must show that the main conversation followed the expected format, and a named subagent stays outside that claim because it uses its own system prompt.
UNKNOWN is not success. It means the selected evidence could not establish the claim. N/A is different: a declared host exception makes the check inapplicable. Neither state should be turned into a green parity badge.
The five-guide implementation series
I am turning this architecture into five practical guides. They remain unpublished while their code samples, cross-links and behavior checks are reviewed, so this article does not link to draft routes that would return a 404.
- Build one configuration for Claude Code and Codex defines the source, release and projection model.
- Share skills and route prompts with BM25 builds provenance, scenarios, thresholds, caches and native-only fallback.
- Install global agent configuration without losing local state implements digest approval, journaling and divergence-aware rollback.
- Make hooks work in Claude Code and Codex separates policy cores from payload adapters and defines activation evidence.
- Audit Claude Code and Codex agent configuration adds a network-free static doctor and separate runtime canaries.
The intended reading order follows the dependency chain. The auditor comes last because it needs stable contracts from the build, installer, router and hook layers. Starting with a dashboard would create a polished report over undefined evidence.
Until then, the inspection report of the running configuration shows the result on one machine at one date: the release pipeline, the 4 layers, the BM25 router and the 3 gaps the inspection found.
The remaining boundary is behavioral
The common source now covers much more than the MCP converter that motivated the first version of this article. It also gives every difference a place: shared policy, host projection, mutable local state or declared exception.
The hard part is no longer copying Markdown. It is keeping claims aligned with evidence as Claude Code and Codex evolve. A release can be structurally valid while a desktop session still uses an older loaded state. A BM25 cache can be fresh while its threshold is poor. A hook can be trusted and invoked while its decision is ineffective.
That is why I no longer attach a portability percentage to the system. The useful measure is coverage per capability and per evidence state. Anything not tested on a named host surface remains UNKNOWN.
YSNK
(You should now know)
- Portable agent configuration starts with one canonical source and produces immutable, host-native releases.
- Claude Code uses
.claude/skills, while this Codex design uses.agents/skills;.codex/skillsis a legacy migration input. - BM25 adds zero to three optional suggestions, bypasses explicit invocation and leaves weak skills available through native discovery.
- Present, installed, trusted, loaded, invoked and effective are separate claims that need separate evidence.
- A Claude-only Output Style is a declared host projection, not evidence that Codex or a named subagent inherited it.
Go Further in the Claude Code Guide
Practical resources selected to help you take the next step.
Open-source galaxy
Projects used in this path
Related articles
1/2 Β· Why I combined three Claude Code skills
Flow Lean fused three response skills. Its historical eval beat each comparator and caught a candidate offering a migration against an inferred target.
2/2 Β· Claude selected my output style. Then ignored it
Claude Code selected flow-lean but skipped its footer. A casing fix showed why installation, selection, and behavior need separate evidence.
1/6 Β· Opposite AI results: what context can explain
Anthropic measured merged PRs; METR measured task duration in a different setting. What these results and long-context research establish, and what remains a hypothesis.
Go deeper
Step-by-step guides that put this into practice.
Claude Code setup, level by level
Three configuration layers for project context, daily tools and persistent memory, with checks for what loads and how it behaves.
Une seule source configure Claude Code et Codex
Une configuration d'agents inspectΓ©e, partagΓ©e par Claude Code et Codex : releases immuables, 4 couches d'exΓ©cution, routage BM25 des skills et Γ©carts relevΓ©s. Rapport complet Γ lire en ligne.
One source configures Claude Code and Codex
An inspected agent configuration shared by Claude Code and Codex: immutable releases, 4 execution layers, BM25 skill routing and the gaps found. Full report to read online.