Guide contents
Other resources

Configuration inspected

Configuring Claude Code
and Codex

Architecture of an agent configuration shared between 2 hosts: release pipeline, 4 execution layers, lexical skill routing.

Inspection dated September 16, 2026 · Extracts re-read · Historical counts not re-verified

Key takeaways

Open the glossary

Terms with a dotted underline show their definition on hover, keyboard focus or tap.

I

Overview

01

A single source feeds Claude Code and Codex

The ai-agents repository is the authority. The files the agents read are rendered artifacts, never hand-edited files.

The configuration is not a set of files dropped into ~/.claude. It is a build pipeline. The source files live in ~/.config/ai-agents/src/ and no client reads them. A render script produces an immutable release from them, and it is that release's artifacts that get installed into the home directory.

This indirection gives both agents a configuration that is consistent by construction. Hand-editing ~/.claude/CLAUDE.md would work, but would leave ~/.codex/AGENTS.md behind, and would produce 2 agents with different judgment on the same code.

The render produces an immutable release, which installation projects to Claude Code and Codex. The next diagram details every installed file.
DIAGRAM · From the source repository to the 2 agents
The render step produces an immutable release; installation projects its artifacts onto the 2 hosts.
The runtime has no npm dependency The pipeline makes no network call and installs no package. Node 22 or newer is enough. The constraint is deliberate, because an agent configuration that depends on a remote registry becomes unavailable exactly when it is needed most.
02

Instructions and skills are suggested, hooks and permissions are enforced

Instructions, skills, hooks, and permissions are not interchangeable. The deciding criterion is the level of guarantee required.

The most common mistake is putting a rule that must be guaranteed into an instructions file. The model will read it, follow it often, and forget it sometimes. If forgetting it is costly, it belongs in a hook, not a sentence.

The 2nd mistake is letting the instructions file accumulate. It stays in the context of every session, on every project. Every line is paid for in context over the entire lifetime of the machine's use. A long procedure reserved for one specific task belongs in a skill: only its description weighs until the body is loaded.

The 2 white layers are suggested to the model, the 2 orange layers are enforced on it. The deciding criterion is the level of guarantee required, never the length of the text to write.
DIAGRAM · Where to place a new rule
Decision tree for choosing the layer for a new rule.
The 4 layers and their cost
LayerFileExecution statusContext cost
InstructionsCLAUDE.md, AGENTS.md, rules/Always in contextPermanent, every turn
Skillsskills/<name>/SKILL.mdLoaded on demandThe description alone, then the body if loaded
Hookssettings.json, hooks.jsonCode executed by the harnessNone, except injected output
Permissionssettings.jsonFilter before executionNone
An instruction is not a guarantee An instructions file is guidance that the model weighs against the rest of the context. A hook is a gate it cannot bypass on the actions that hook intercepts; an action taken through another path escapes it. Confusing the two produces a false sense of security, because the rule is written, looks enforced, and slips at the exact moment the context is saturated.
II

The pipeline and the layers

03

A release is identified by the SHA-256 of its manifest

The render step produces an immutable directory. Its identifier is the digest of its normalized manifest, not a version number.

scripts/render.mjs reads src/, produces a directory under releases/, and names it after the SHA-256 of its normalized manifest. 2 renders of the same content produce the same identifier; a single byte of difference produces a different directory. The current pointer is a symbolic link to the active release.

The sourceCommit field is only the base Git reference. The manifest separately binds the actual content through sourceProvenance.contentDigest, and every rendered byte through manifest.artifacts. The distinction matters because a render produced from a modified working tree stays traceable, whereas a plain commit hash would lie.

JSON Extract of artifact-manifest.json 16 lines
{
  "schemaVersion": 1,
  "releaseId": "f4528d92…e657847",
  "sourceCommit": "27cb7932…866d0e6",
  "sourceProvenance": {
    "commitRole": "base-reference-not-content-proof",
    "contentBinding": "manifest.sources",
    "contentDigest": "f5e7ea7a…8c5a0067",
    "releaseBinding": "manifest.artifacts"
  },
  "requirements": { "node": ">=22" },
  "artifacts": {
    "claude/CLAUDE.md": { "hash": "8f9790e7…", "mode": 420, "type": "file" },
    "codex/AGENTS.md":  { "hash": "…", "mode": 420, "type": "file" }
  }
}
What a release contains
ArtifactInstalled destination
claude/CLAUDE.md~/.claude/CLAUDE.md, rendered flat, with no mutable @ import
codex/AGENTS.md~/.codex/AGENTS.md
claude/output-styles/flow-lean.md~/.claude/output-styles/ and selection in settings.json
reference/ANTI_AI.mdEditorial reference, loaded on demand only
hooks/Anti-marker adapters, BM25 router, Git checkpoint
skills/common/95 normalized skills
skills/projections/claudeCopied to ~/.claude/skills/
skills/projections/codexTarget of the ~/.agents/skills link
artifact-manifest.jsonFingerprint of every rendered byte

The installer uses an exclusive lock, compares the exact preimages of the live files, writes atomically target by target, and logs durably. A stale preimage invalidates the approval: if a file has changed between validation and writing, the transaction stops. Rollback only restores a target that still matches its recorded postimage.

UNKNOWN is not success Any unexplained divergence stops the installation with exit code 6. That is the right posture for a tool that writes into the home directory, because a readable failure is better than a partially applied state whose shape nobody knows.
Checking installation state without changing anything
Terminal Terminal 8 lines
# which release is active
readlink ~/.config/ai-agents/current

# drift between the live Claude copy and the projection
node ~/.config/ai-agents/scripts/check.mjs --home

# state of both skill roots
node ~/.config/ai-agents/scripts/inventory.mjs

check.mjs --home reports LIVE_CLAUDE_SKILLS_DRIFT when the real copy differs from the active projection, and LIVE_CODEX_SKILLS_DRIFT when the Codex link is missing or points elsewhere. The checker reports, it never repairs. Separating diagnosis from repair keeps an automatic repair from masking the cause.

04

Claude receives a real copy, Codex a symbolic link

The asymmetry is not an aesthetic choice. It works around 2 documented Claude Code bugs.

The 2 hosts do not read skills from the same place. Claude Code reads ~/.claude/skills, Codex reads ~/.agents/skills then ~/.codex/skills. The release provides one projection per host, but they are not installed the same way.

Skill roots and installation mode
HostRoot readModeReason
Claude Code~/.claude/skills/Real copy, replaced atomicallyA symlinked root can be ignored, and auto-update can delete the link
Codex~/.agents/skillsSymbolic link to the projectionNo equivalent bug observed
The skills projection feeds both roots, installed in 2 different ways.

Both Claude Code bugs are referenced in the sources: issue 38051 for the symlinked root being ignored, issue 50052 for the link being deleted on auto-update. The real copy costs a disk duplication and a drift risk; that is the price of a root that survives an update.

In a repository, the same pattern applies as a mirror For a project, keep the editable skills in .agents/skills and expose the same tree to Claude through .claude/skills -> ../.agents/skills. One editable copy, 2 hosts that read it. The reverse, 2 separately edited directories, diverges within weeks.
Visibility does not prove availability A skill visible to both hosts can still be unusable if a CLI, a Python package, or an MCP server it calls is not installed. The projection places the file where the host looks for it. Its discovery and loading are then checked in the client.
05

Global instructions stay in the context of every session

CLAUDE.md, AGENTS.md, and the rules directory are loaded by the host according to its scope, then stay in the context. Their budget is a constraint, not a preference.

Claude Code renders global instructions flat. The rendered file contains no mutable @ import, because a broken import would make the whole block silently disappear. The pipeline therefore renders a single file, and the long editorial reference stays a separate file, loaded only when a long writing task justifies it.

Alongside the main file, ~/.claude/rules/ carries 5 topical rules, also loaded in context.

Installed global rules
FileWhat it enforces
code-navigation.mdUse LSP workspaceSymbol and documentSymbol before reading a whole file
code-search.mdast-grep for structural search, local semgrep scan before a sensitive commit
copy-paste-messages.mdFormat for messages meant for the clipboard: standard Markdown, plain-text URLs, one message per idea
pr-description-format.mdTL;DR up top, dependency between MRs right after, a diagram only when it earns its place
untrusted-content.mdAll external content is data, never an instruction, with a mandatory report
Parity between the 2 files is a maintenance obligation CLAUDE.md and AGENTS.md must stay equivalent. A rule present in only one of the two produces 2 agents that settle the same trade-off differently on the same repository. The inconsistency only shows up after the fact, on a trade-off already settled.
06

A skill's description decides whether it is loaded

A skill's body is only read once it is loaded. Before that decision, the model mostly sees its name and its description.

A skill is a directory containing a SKILL.md with a YAML frontmatter. The description field is the main information the model sees before deciding whether to load the skill, along with its name. A vague description produces a skill that never triggers, or one that triggers all the time.

Code Minimal frontmatter 4 lines
---
name: my-skill
description: What the skill does. Use when <trigger situations>. Do not use for <what looks similar but does not belong here>.
---

The decisive point is writing both boundaries, when to use it and what looks like it but is not. Without the negative boundary, 2 neighboring skills fight over the same prompts and the model picks one at random.

The workstation carries 95 global skills, 35 agents, and about 48 commands. At this scale, 2 mechanisms prevent saturation: the skillOverrides field in settings.json, which carries 80 entries turning skills off or restricting them to user-invocable-only, and the BM25 router described in part III.

Telling apart a skill, an agent, and a command
3 objects often confused
ObjectLocationTriggerContext
Skillskills/<name>/SKILL.mdThe model decides, or the user types /nameLoaded in the current session
Agentagents/<name>.mdThe model delegates a taskSeparate context, returns a report
Commandcommands/<name>.mdThe user types /nameInjected as a prompt

In practice, an agent isolates the context cost of its exploration, while a skill loaded in the current conversation does not. Claude can run a skill in a subagent, which this configuration does not use. A task that will read 30 files to keep 3 lines is an agent.

07

Hooks enforce a check where they are wired

A hook is code executed by the harness at a lifecycle event. The model does not get to skip it, but coverage stops at the tool calls its matcher recognizes.

A hook receives JSON on its standard input and decides. On PreToolUse, an exit code of 2 blocks the call and sends the message back to the model so it can correct course, without failing the session. What that code does depends on the event: on UserPromptSubmit it rejects the prompt, and on PostToolUse the tool has already run. This mode is preferable because a hook that breaks a session ends up disabled, and is then worth nothing at all.

DIAGRAM · A prompt's lifecycle and hook points
Hooks sit between the user, the model, and the tools.
Hooks wired in settings.json
EventHookRole
UserPromptSubmitsmart-suggest.shSuggests commands and agents
UserPromptSubmitskill-router/bm25-suggest.jsSuggests skills by BM25 score
PreToolUse:Bashblock-env-reads.shBlocks reading environment files
PreToolUse:Bashlint-commit-message.shChecks the commit message before execution
PreToolUse:Bashrtk hook claudeCLI tooling integration
PreToolUse:Readblock-private-files.shBlocks reading private files
PreToolUse:Edit|Writeclaude-adapter.shAnti-marker checks on written text
PreToolUse:Agentmodel-usage-tracker.shTraceability of launched subagents
PreToolUse:* and PostToolUse:*git-ai checkpointSilent checkpoint of the working tree
PostToolUse:Bash|Readmask-credentials.shTransforms the received output before re-emitting it
SessionStartrtk-baseline.shCaptures the starting state
SessionEndauto-rename-session.sh, session-summary.shSession rename and summary
Codex adds cryptographic trust per hook ~/.codex/config.toml stores a [hooks.state."<file>:<event>:<i>:<j>"] block with a trusted_hash for every declared hook. Modifying the script invalidates the hash and the hook stays inactive until explicit re-approval. This is the protection against a hook being silently modified, whether by a third party or by an agent.
Codex wiring is separate from its configuration Codex reads the wiring from ~/.codex/hooks.json, but keeps the trust state in ~/.codex/config.toml. Modifying one without the other produces a hook that is declared and never executed, with no error message.
JSON Wiring in ~/.claude/settings.json 23 lines
"PreToolUse": [
  {
    "matcher": "Edit|Write",
    "hooks": [
      {
        "type": "command",
        "command": "~/.config/ai-agents/releases/d16f4a6c…866bfc84/hooks/claude-adapter.sh",
        "timeout": 5
      }
    ]
  }
],
"UserPromptSubmit": [
  {
    "hooks": [
      {
        "type": "command",
        "command": "env SKILL_ROUTER_HOST=claude SKILL_ROUTER_DATA_DIR=\"~/.local/state/ai-agents/skill-router/claude\" node \"~/.config/ai-agents/releases/d16f4a6c…866bfc84/hooks/skill-router/bm25-suggest.js\"",
        "timeout": 5
      }
    ]
  }
]
Installation does not prove that outputs are masked mask-credentials.sh reads the event JSON, transforms it with jq and re-emits it. It does not build the hookSpecificOutput.updatedToolOutput field, the only documented way to replace a tool output. Its presence in settings.json therefore shows an intent, not an effective replacement of the output before it reaches the model. Checking that requires a dummy output in a live session, which was not done.
08

3 independent barriers filter before execution

deny, ask, and sandbox operate at different levels. None replaces the other two.

The 3 mechanisms do not overlap. deny outright refuses a command or path pattern. ask pauses to request confirmation. The sandbox is OS-level isolation, applied to each shell command taken separately.

What each barrier covers
BarrierLevelInstalled content
permissions.denyCommand or path patternEnvironment files, *.pem, *.key, ~/.ssh, ~/.aws, git push --force, git reset --hard, deleting a remote repository
permissions.askHuman confirmationPush, merging an MR or PR, publishing a package, production deployment
sandbox.networkNetwork egressAllowlist of 44 domains, everything else refused
sandbox.filesystemDisk writesAllowlist of 22 write directories and 2 read entries
sandbox.credentialsSecrets5 paths and 13 API variables refused to commands
sandbox.excludedCommandsExceptions95 exclusion entries as of September 22, 2026, for commands that need network or credentials
The 3 barriers operate at different levels and do not replace one another. Their shared scope stops at the agent.

The excludedCommands list takes commands out of isolation, so it deserves a careful read. That is necessary for gh, docker, or package managers, but every entry is a deliberate breach. It has to stay justifiable line by line.

These barriers protect the agent, not a human A deny rule stops the local agent from reading a file. It stops nothing at all for a person who clones the repository. A repository containing client data does not get shared on the grounds that a local configuration has a deny on those paths. The 2 problems do not share a scope, and they do not share a solution.
JSON Extract of ~/.claude/settings.json 26 lines
"permissions": {
  "defaultMode": "auto",
  "deny": [
    "Bash(git push --force *)",
    "Bash(git reset --hard *)",
    "Bash(gh repo delete *)",
    "Read(**/.env)",
    "Read(**/*.pem)",
    "Read(**/.ssh/**)"
  ],
  "ask": [
    "Bash(git push *)",
    "Bash(gh pr merge *)",
    "Bash(npm publish *)",
    "Bash(vercel *--prod*)"
  ]
},
"sandbox": {
  "enabled": true,
  "autoAllowBashIfSandboxed": true,
  "allowUnsandboxedCommands": false,
  "network":     { "allowedDomains": ["<44 domains>"] },
  "filesystem":  { "allowWrite": ["<22 directories>"], "allowRead": ["<2 entries>"] },
  "credentials": { "files": ["<5 paths>"], "envVars": ["<13 variables>"] },
  "excludedCommands": ["gh *", "glab *", "docker *", "<92 more>"]
}
III

The BM25 router

09

Lexical routing avoids a network call per prompt

3 approaches were possible for choosing which skills to flag. Cost and reproducibility settle it.

An agent with 95 skills would keep as many descriptions in context, before any body is loaded. It has to decide, before every turn, which ones deserve to be flagged.

3 approaches to routing
ApproachCost per promptReproducibleDependencies
Reading all 95 descriptionsConstant, non-zero contextNo, the decision degrades with sizeNone
EmbeddingsNetwork call or local modelNo, between model versionsModel, network, or binary
Lexical BM25A few milliseconds claimed on a cached index, with no measurement attachedYes, with an identical index and contextNone

BM25 was chosen. The accepted trade-off is that the router compares words, not meanings. In exchange, it is debuggable, since a score is a readable sum of per-term contributions.

10

BM25 scores relevance with 3 components

The score combines saturated frequency, length normalization and term rarity, and each constant answers a specific problem.

BM25, or Okapi BM25, is the default ranking in Lucene, Elasticsearch, and SQLite FTS5. For every query term q present in document D, it computes a contribution, and the total score is the sum over all the query terms.

Code Formula 12 lines
score(q, D) = IDF(q) × ( f(q,D) × (k1 + 1) )
                       ÷ ( f(q,D) + k1 × (1 - b + b × |D|/avgdl) )

IDF(q) = log( 1 + (N - n + 0.5) / (n + 0.5) )

f(q,D)  frequency of term q in document D
|D|     document length
avgdl   average document length in the corpus
N       total number of documents
n       number of documents containing q

Constants used: k1 = 1.2, b = 0.3
The 3 components of the formula, and what each one corrects. The total score is the sum of these contributions over the words of the query.

`k1 = 1.2`, saturation. This is what prevents linearity. A document containing the word sitemap 10 times is not 10 times more relevant than a document containing it once. The function rises fast, then plateaus.

Frequency factor for k1 = 1.2, with document length equal to the average and IDF set aside
Frequency fFrequency factorMarginal gain
11.00reference
21.38+0.38
31.57+0.19
51.77+0.20 over 2 occurrences
101.96+0.19 over 5 occurrences
infinite2.20theoretical ceiling

`b = 0.3`, length normalization. Without it, a long document wins mechanically, because it contains more terms, so more chances of matching. The |D|/avgdl factor penalizes documents longer than average. b = 0 disables normalization, b = 1 applies it fully.

The 0.3 value is low, deliberately. The corpus is made of sentences of 5 to 15 words, where length carries almost no information. A b close to 1 would unfairly penalize a detailed phrasing against an isolated keyword.

The detail that matters in the IDF

IDF, or inverse document frequency, is the core of the system. A term present in every document discriminates nothing. A rare term discriminates a lot.

The 1 + at the head of the formula is the detail that changes the behavior. The classic Robertson and Sparck Jones form, without that 1 +, turns negative as soon as a term appears in more than half the corpus. A frequent term would then actively penalize the documents that contain it, which makes no sense for this use case. The floor at zero avoids that behavior.

Upstream, a bilingual tokenizer prepares the text: camelCase splitting, lowercasing, accent folding, removal of about 60 French and English stopwords, negation detection, then stemming using a list of 25 suffixes from both languages. Function words like le, la, de therefore never reach the computation.

JavaScript routing/bm25.js, the scoring core 27 lines
const K1 = 1.2;
const B = 0.3;

function computeIdf(docs) {
  const N = docs.length;
  const df = new Map();
  for (const doc of docs) {
    for (const t of new Set(doc.tokens)) df.set(t, (df.get(t) || 0) + 1);
  }
  const idf = {};
  for (const [t, n] of df) idf[t] = Math.log(1 + (N - n + 0.5) / (n + 0.5));
  return idf;
}

function scoreDoc(queryTokens, doc, idf, avgdl) {
  const tf = termFreq(doc.tokens);
  const dl = doc.tokens.length || 1;
  let score = 0;
  for (const q of queryTokens) {
    const f = tf.get(q);
    if (!f) continue;
    const num = f * (K1 + 1);
    const den = f + K1 * (1 - B + B * (dl / avgdl));
    score += (idf[q] || 0) * (num / den);
  }
  return score;
}
11

From score to suggestion, 4 conditions to clear

BM25 alone produces a number. The maximum per skill, a calibrated threshold, a negative veto, and an eligibility selection produce a decision.

A raw score does not say whether to suggest. That is where the router's quality is decided, not in the formula.

The full chain, from prompt to injected suggestions. The next diagram shows every exit without a suggestion.
DIAGRAM · From prompt to suggestion
Processing chain, with the 4 successive filters.
  1. Maximum per target, never the sum

    A skill's score is that of its single best scenario. Summing would mechanically favor a skill with 40 scenarios over one with 10, regardless of relevance.

  2. Threshold tau calibrated per target

    Every skill gets its own threshold, computed when the index is built. A target with fewer than 8 positive scenarios or fewer than 2 negatives is excluded from calibration, because below that the threshold would be tuned on noise. Positives are scored against the other positives of their own target, excluding themselves, otherwise the score is perfect by construction.

  3. The threshold is calibrated on the skill's declared negatives

    Threshold calibration looks only at the skill's own scenarios: its positives, scored against its other positives, and its declared negatives. The comparison between neighboring skills comes later, in the cross-skill evaluation.

  4. Sweeping every candidate threshold

    The threshold chosen is the one that maximizes F-beta over the whole set of observed scores, by testing every midpoint between 2 consecutive scores. The skill reaches status ok when the F1 measured at that threshold reaches 0.60, and stays in conflict otherwise.

  5. The cross-skill evaluation decides eligibility

    A skill calibrated to ok is not suggestible yet. The builder replays every skill together, keeps those whose cross-skill F1 reaches 0.55, then drops the weakest one while the global F1 stays below 0.70. Only the skills that survive that pass carry eligible.

The optimization is asymmetric. The F-beta used sets beta = 2, which makes recall weigh 4 times more than precision. A missed suggestion is a skill that goes unused. An extra suggestion is a line the model ignores. The policy therefore accepts false positives to reduce missed suggestions.

DIAGRAM · The 3 states of a target after calibration
A target is only suggestible in the ok state.
The negative veto, and what it does not cover A single rare term in common is enough to clear tau on a text that is otherwise off-topic, because a high IDF carries the score on its own. The release router answers that case with the negative veto: when the best score reached by a negative scenario matches the positive score, the candidate is dropped. The floor of 2 shared tokens belongs to the legacy router, where MIN_SHARED_TOKENS is 2.
Inspecting and recalibrating the index
Terminal Terminal 12 lines
# index summary, without writing anything
SKILL_ROUTER_HOST=claude node \
  ~/.config/ai-agents/current/hooks/skill-router/routing/build-index.js --dry-run

# rebuild the index
SKILL_ROUTER_HOST=claude node \
  ~/.config/ai-agents/current/hooks/skill-router/routing/build-index.js

# test a suggestion end to end
echo '{"prompt":"prépare un message pour l équipe"}' \
  | SKILL_ROUTER_HOST=claude node \
      ~/.config/ai-agents/current/hooks/skill-router/bm25-suggest.js

The hook rebuilds the index on its own in the background whenever a scenario file changes. --dry-run computes everything without writing the cache, and prints a JSON summary: scope, number of scenarios, covered skills, eligible skills, conflicts, and exclusions. It prints neither the tau values nor the per-skill F1 scores, which are read from the cache files after a real build. These commands target the author's private installation; on another machine the paths differ.

JavaScript Maximum per skill, then eligibility 28 lines
function scoreSkills(queryTokens, scenarios, index) {
  const bySkill = new Map();
  const negativeBySkill = new Map();
  for (const s of scenarios) {
    const raw = scoreDoc(queryTokens, s, index.idf, index.avgdl);
    if (s.polarity === 'neg') {
      if (raw > (negativeBySkill.get(s.skill) || 0)) negativeBySkill.set(s.skill, raw);
      continue;
    }
    if (s.polarity !== 'pos') continue;
    if (raw > (bySkill.get(s.skill) || 0)) bySkill.set(s.skill, raw);
  }
  // One score per skill: its best scenario, never the sum.
  return [...bySkill]
    .map(([skill, score]) => ({ skill, score, negativeScore: negativeBySkill.get(skill) || 0 }))
    .sort((a, b) => b.score - a.score);
}

function filterEligible(scored, thresholds, activeSkills) {
  return scored.flatMap((candidate) => {
    const threshold = thresholds[candidate.skill];
    const active = activeSkills[candidate.skill];
    if (!threshold || threshold.status !== 'ok' || threshold.eligible !== true || !Number.isFinite(threshold.tau)) return [];
    if (Number.isFinite(candidate.negativeScore) && candidate.negativeScore >= candidate.score) return [];
    if (candidate.score < threshold.tau || !active || !active.skillMd) return [];
    return [{ ...candidate, skillMd: active.skillMd }];
  });
}
12

BM25 is lexical, and the corpus is an artifact to maintain

3 structural limits, 2 of which are fixed through the corpus and one only through a change of approach.

2 phrasings of the same need, without a single word in common. The router has nothing to measure, so it says nothing.
Semantic limit "rends ce texte moins bavard" (make this text less wordy) will not match a skill whose corpus only contains "concise" and "lean". The fix is to write both phrasings into the corpus, not to change the algorithm.
The corpus ages like code A skill whose real triggers have drifted keeps a corpus that describes the old usage, and starts missing the real prompts. Nothing flags it, because the router keeps working and simply routes toward the past.
Calibration only holds on its own corpus The metrics shown by --dry-run are measured on the data used to calibrate. They detect a regression between 2 versions of the corpus. They do not prove quality in real use. A test set written independently of the calibration corpus, then frozen, measures something else. A held-out validation set would require logging real prompts, which current logging does not do, by a deliberate privacy choice.
JSON Extract of the flow-lean corpus, 6 positives and 2 negatives out of the 40 scenarios in the file 15 lines
{
  "skill": "flow-lean",
  "positive": [
    "mode lean activé",
    "passe en mode léger",
    "brevity mode on",
    "trim the fat",
    "donne-moi le TLDR de cette session",
    "récapitule les décisions prises dans cet échange"
  ],
  "negative": [
    "écris-moi un article complet",
    "donne moi tous les détails"
  ]
}
IV

Reuse and go deeper

13

The starter kit carries the 4 layers without the personal inventory

agent-config-starter carries the structure, not the content. It exists on the inspected workstation and is not published to date.

Not published as of September 18, 2026 The starter kit only exists on the inspected workstation. No public repository distributes it to date. This chapter describes its structure and its installation choices, for anyone who wants to rebuild the equivalent.

Handing over this configuration as-is would make no sense, because it contains project paths, MCP servers with their keys, and 35 agents most of which serve only one specific purpose. The starter kit carries the structure and a standalone BM25 router, with 2 example skills so the mechanics are verifiable right from installation.

Code Starter kit tree 11 lines
agent-config-starter/
├── README.md                     the 4 layers, when to use which
├── install.sh                    dry run by default, --apply to write
├── docs/bm25.md                  the full algorithm
├── claude/CLAUDE.md              generic global instructions
├── claude/settings.json          permissions + hooks
├── codex/AGENTS.md               mirror
├── codex/config.toml.example     commented, no secrets
├── hooks/anti-ai-markers.sh      PreToolUse hook, exit 2 = fix yourself
├── hooks/skill-router/           standalone BM25 router, 4 files
└── skills/<name>/                example skills with corpus

Its install script runs in dry-run mode by default and only writes with --apply. It backs up to .bak.<timestamp> before any overwrite, and never merges an existing settings.json. It drops a .new file next to it and leaves the merge to a human. Automatically merging configuration JSON overwrites an existing field or resolves a conflict wrongly, without saying so.

Checks performed on September 16, 2026 The 2 example skills calibrate to status ok (F1 0.88 and 0.92). The router suggests the right skill on 2 targeted prompts and stays silent on an off-topic prompt and on a prompt carrying the opt-out phrase. The anti-marker hook exits 2 on a marker in prose, and exits 0 on clean text and on a marker inside a code block. install.sh ran end to end in dry-run mode.
What is deliberately absent MCP servers, model-switching wrappers, specialized agents, project paths, and the release pipeline. The recipient starts from a structure to fill in, not from an inventory they would need to understand before being able to change anything.
14

3 gaps found during the September 16, 2026 inspection

A latent mismatch, a costly redundancy, and an oversized guardrail in the legacy router.

The 3 gaps found during the inspection. None of them raised an error.
Hooks are pinned to a release earlier than the current pointer current points to a September 15 release, but settings.json and hooks.json hard-reference a September 6 release. diff -rq between the 2 hooks/ directories returns nothing, so the code is byte-identical and there is no functional gap today. The day a hook changes, the current release will carry the new code and both hosts will keep silently running the old one.

This behavior is consistent with the design, in which activating hooks is a separate transaction, distinct from installing instructions and skills. It requires explicit approval tied to a digest. The practical consequence is that this transaction has to be replayed after every hook change, and nothing reminds anyone of that.

The legacy router is installed, but the September 22 wiring does not run it The old router, under ~/.claude/hooks/routing/, targets agents and commands. It carries the floor of 2 shared tokens and the anti-duplicate guard described below. As of September 22, 2026, settings.json wires the release BM25 router and smart-suggest.sh on UserPromptSubmit, a Bash script of regular expressions that never calls Node. The legacy file is therefore present without being executed by that wiring. The source guide described 2 active BM25 routers on September 16; no snapshot of settings.json from that day ships with this report, so the observation stays declared and not re-verified.

The two cover different objects and are therefore not strictly redundant. The question to settle is whether routing agents and commands still deserves a separate engine, or whether the 2 corpora can merge into the host-aware router.

The legacy router's anti-duplicate guardrail is too broad ~/.claude/hooks/routing/bm25-suggest.js drops a target whose name appears anywhere in the prompt, through a plain substring test. The intent is sound, since the guard should not re-suggest a tool the user just named. The implementation is less so, because a target named after a domain word disqualifies itself. A prompt containing the word debugger drops the debugger agent.

The release router does not carry this flaw, because its guardrail requires the name preceded by the sigil, /name for Claude and $name for Codex, which tells apart an invocation from a vocabulary word. This is the version to adopt if the legacy router is kept. The reusable starter kit was fixed in this direction, and a test case covers it.

15

11 published pieces detail each layer

This report describes an installation at a given point in time. The articles and guides below detail the reasoning layer by layer.

This report shows one configuration, as inspected on September 16, 2026. The pieces below, published on the same site, take each layer separately and lay out the reasoning, the measurements, and the mistakes that led to this shape.

Related content, by layer
Related content, by layer
LayerContentWhat it adds
Pipeline and releasesPortable agent configuration is a release system, not a shared folderThe stable map of the system: source, build, install, runtime, audit, and what still varies between the 2 hosts
PortabilityPortability becomes a Scale concernWhy native primitives are not enough: neutral sources, generated outputs, release control, behavior tests
Global instructionsYour CLAUDE.md is too longWhat belongs in the instructions file, in a procedure, or in a response preference, and how to check loading
SkillsWhy I combined three Claude Code skillsMerging 3 response skills into one, and the evaluation that settled it
Output StylesClaude selected my output style. Then ignored itInstallation, selection, and behavior require separate evidence
Hooks and MCPClaude Code security: the attack surface nobody auditsA hook runs with the user's permissions, an MCP server is third-party code
MCP costMCP servers: what they actually cost and when to use themImmediate or deferred tool loading, context consumed, and tokens billed
Getting startedClaude Code setup, level by level3 configuration levels, with what to check at each one
DiagnosingContext engineering: the L0-to-L5 playbookChoosing a context control based on the observed failure
As a teamThe AI instruction system is a product, not a config fileThe move from a personal CLAUDE.md to a system shared by 6 developers
In a projectFrom afterthought to infrastructure9 months of AI configuration in a production project
Where to start For an overview, read the article on portable configuration first. For a first install, start from the level-by-level guide. For a specific problem, the L0-to-L5 guide starts from the symptom.

A question about this configuration?

Have a question, experience or correction to share? Message me on LinkedIn.

Contact me on LinkedIn
16

Glossary

Terms used in this report.

BM25 / Okapi BM25
Lexical ranking function that scores a document's relevance to a query from term frequency, term rarity, and document length. Default ranking in Lucene, Elasticsearch, and SQLite FTS5.
Hook
Script executed by the harness at a session lifecycle event. Receives JSON on its standard input. An exit code of 2 sends a correction message back to the model without failing the session.
Projection
Skill tree rendered by a release for a given host. Claude receives a real copy, Codex a symbolic link.
Skill
Directory containing a SKILL.md file with frontmatter. Loaded on demand. Only its description is seen before the decision to load it.
Other glossary terms
Agent
A subagent launched by the main model in a separate context, which returns a report. Isolates the context cost of its exploration, unlike a skill.
avgdl
Average document length in a corpus, in number of tokens. Used as the reference for length normalization in BM25.
Corpus
Set of example sentences declared by a skill in its scenario file, split into positives and negatives.
F-beta
Weighted harmonic mean of precision and recall. With beta = 2, recall weighs 4 times more than precision.
F1
Harmonic mean of precision and recall. A value of 1 means no false positive and no false negative on the measured set.
Harness
The client software that organizes the session, loads the instructions, calls the model and runs the tools. Claude Code and Codex are 2 harnesses. It is the harness that enforces hooks and permissions, not the model.
IDF
Inverse document frequency. Weight given to a term based on its rarity in the corpus. A term present everywhere discriminates nothing, a rare term discriminates a lot.
Immutable release
Directory produced by the render step, identified by the SHA-256 of its normalized manifest, and never modified after creation.
Manifest
File that binds every rendered byte of a release to its fingerprint. Its normalized digest serves as the release's identifier.
MCP
Model Context Protocol. Protocol through which an external server exposes tools to an agent. The context cost depends on the metadata actually loaded and on the tool discovery mode, immediate or deferred.
Permission
Filter applied before a tool executes. 3 modes: allow, which authorizes without asking; ask, which pauses for confirmation; deny, which refuses outright.
Precision
Share of the suggestions issued that were relevant. Low precision produces noise.
Recall
Share of the relevant cases actually suggested. Low recall produces skills that never trigger.
Sandbox
OS-level isolation, applied to each shell command taken separately. Limits network, disk writes, and access to secrets.
SHA-256
A digest function producing a 256-bit fingerprint. Two identical contents give the same fingerprint; a single byte of difference gives another one.
Stemming
Reduction of a word to its root by stripping suffixes, so that "routing" and "routed" produce the same token. The tokenizer used here strips English and French suffixes from the same list.
Stopword
Function word removed before the score is computed because it carries no discriminating power. Here, about 60 French and English words.
Tau
Score threshold specific to each skill, computed when the index is built. Below it, no suggestion is issued.
trusted_hash
Fingerprint of a hook script recorded by Codex. Modifying the script invalidates the fingerprint and disables the hook until explicit re-approval.
17

Conclusion, sources and updates

Report scope and method

Describes the global configuration installed on the workstation as of September 16, 2026, as observed through direct inspection of the files. The counts and checks dated that day come from the source guide and have not been re-verified since; the code and configuration extracts, on the other hand, were re-read in the files. Covers the ai-agents pipeline, the 4 execution layers, the BM25 router, and the reusable starter kit. Does not cover repository-specific configurations, individual MCP servers, or third-party plugins.

Conclusion

The value of this configuration is not in the number of skills, nor in how fine-tuned the router is. It is in a clean separation between what is suggested to the model and what is imposed on it, and in a pipeline that keeps the 2 hosts consistent as long as the installed release is checked.

Each of the 3 gaps calls for a disposition. Pinning the hooks to a release earlier than the current pointer has no effect today, but it will go stale with no signal: the activation transaction has to be replayed after every hook change. The legacy router sits on disk without being wired: either it gets a deliberate wiring back, or it is removed. Its anti-duplicate guard has to be fixed before any new wiring, as it already was in the starter kit.

To pass on the method without passing on the inventory, the starter kit's structure is enough: the 4 layers, a verifiable router, and 2 example skills, which the recipient fills in at the pace of their real needs. The starter kit itself is not published to date.

Dated sources

“Accessed on” gives the date the source was read. A source may have been published earlier and later updated.

Update history

DateVersionChanges
22 September 20263.0.0Corrections from an independent review. The router decision chain is rewritten on the release code: explicit opt-out instead of a negation cut-off, negative veto instead of the floor of 2 shared tokens, and cross-skill evaluation before eligibility. The shared-token floor and the anti-duplicate guard go back to the legacy router, described as installed but not wired as of September 22, 2026. The hook guarantee is scoped to intercepted actions, output masking moves from acquired effect to unproven intent, and exit 2 depends on the event. The scoreSkills extract regains its sort, the quoted corpus uses 2 real negatives, the sandbox counts move to today's values, and the "one key in 10" figure is dropped for lack of measurement. 3 infographics regenerated.
20 September 20262.3.0Added 6 extracts taken from the real files: a release manifest, the hook wiring, the permissions and the sandbox, the BM25 scoring core, the maximum per skill with the eligibility filter, and an extract of the flow-lean corpus. Personal paths are reduced to ~, digests are truncated, and the lists of domains, directories and secrets are replaced by their counts.
19 September 20262.2.0The infographics move to the BoldGuy design system and form a numbered series from 01/08 to 08/08. The 4 existing ones are redone (4 layers, 3 barriers, 3 BM25 components, lexical limit) and 4 new ones are added: the pipeline from source to 2 agents, the real copy versus the symlink, the chain from prompt to suggestion, and the 3 gaps found. The Mermaid diagrams stay in place. 17 calls to Gemini 3 Pro Image, 1 of them rejected for a missing space in a title.
19 September 20262.1.4Numbers now appear as digits throughout the text, including titles, captions, diagrams and history: "4 layers" instead of "four layers". Words remain for pronouns ("the other two"), the text copied from the infographics and quoted article titles.
19 September 20262.1.3Announcement punctuation pass. Colons that introduced an explanation become a causal connector or 2 sentences, the TL;DR question becomes a statement, and the 4-layer chapter title names the fact instead of announcing a question. The last leftover emphasis phrase is removed.
19 September 20262.1.2Cadence pass. Verbless TL;DR labels become sentences, noun lists written as sentences are rewritten with a verb, and the starter kit verification note moves from log style to full sentences.
19 September 20262.1.1Editorial review. The TL;DR announced 2 gaps while the chapter describes 3: the over-broad guard of the older router is now listed. French placeholders left in code spans are translated.
18 September 20262.1.0Public edition. The agent-config-starter starter kit is described as unpublished and the install command block is removed, since no repository distributes it. Added a chapter linking each layer to the 11 published articles and guides on the topic, and links to the portfolio, the Claude Code guide, and GitHub. New styling in the portfolio's colors.
16 September 20262.0.0Replaced the decorative illustrations with 4 constructed infographics, generated with Gemini 3 Pro Image: the 4 layers, the 3 components of the BM25 score, the 3 barriers before execution, and the router's lexical limit. Each one carries a title, captioned cards, and a summary line. The 5 structural diagrams stay in Mermaid. The 4 files ship as WebP, 90 percent lighter than native JPEG at equivalent visual quality; the originals stay in images/originals/.
16 September 20261.4.0Added 2 illustrations generated with Gemini 3 Pro Image: an abstract opening in the brand's colors, and a visual rendering of the router's lexical limit. The 5 structural diagrams stay in Mermaid, because their edges carry meaning and a generative render would distort them.
16 September 20261.3.0Added an infographic of the 4 layers, generated with Gemini 3 Pro Image. It complements the decision tree without replacing it: diagrams where every edge carries meaning stay in Mermaid.
16 September 20261.2.0Moved the glossary to the head of the document, before the summary and the chapters, so the vocabulary is available from the first read.
16 September 20261.1.0Added a 3rd gap found: the legacy router's anti-duplicate guardrail tests a plain substring, which means a target named after a domain word disqualifies itself. The release's router requires the sigil and does not have this flaw.
16 September 20261.0.0Created. Architecture of the ai-agents pipeline, 4 execution layers, BM25 router, and reusable starter kit, based on a direct inspection of the workstation. 2 gaps found and documented: hooks pinned to a release earlier than the current pointer, coexistence of 2 BM25 routers.

Copy the report as JSON

Automatic copying is unavailable. The text is selected: press ⌘C or Ctrl+C, then paste it into your assistant.

Share this report

Download the HTML report