Convention shipped with ThisCode / ThisCodex. Origin: 운영자 spec 2026-05-16 — "규칙을 CLAUDE.md/soul.md/메모리에 많이 넣으면 다 기억 못 함. rules.md 신설해 상황별 참조." Verified in the obsidian-ai-vault deployment (
.claude/rules/).
Front-loading every operational rule into the always-loaded context (CLAUDE.md / AGENTS.md / soul.md / auto-injected memory) bloats the context window. Recall degrades — the agent "knows" the rules are somewhere but does not reliably apply the right one at the right moment. Adding more rules makes it worse, not better.
A rules/ directory with one small router + many on-demand topical files:
.claude/rules/ (or <bot-WD>/rules/ for a Codex bot)
├── INDEX.md ← the ONLY file referenced from always-loaded context
├── discord-comms.md ← loaded only when about to talk to an external channel
├── source-fact.md ← loaded only when asserting a fact / verifying
├── autonomy.md ← loaded only when tempted to over-confirm / before "done"
│ (also: the proactive completion/partial/blocked report
│ gate — report on completion, partial artifact,
│ awaiting-permission, handoff, or blocked >10–15min
│ WITHOUT being pinged; "next step remains" is not a
│ reason for silence. Bridge-level heartbeat backs this
│ in code — see ThisCodex docs/yolo-bridge-contract.md)
├── skill-process.md ← loaded only when starting a build/debug/verify task
├── porting-infra.md ← loaded only when porting/deploying/adding MCP
├── voice.md ← loaded only when writing a persona response
├── image-ops.md ← loaded only when generating/editing images
│ (edit vs generate vs overlay; real people/brands/
│ products are reference-first, no-imagination)
├── orchestration.md ← loaded only when delegating to / waiting on a bot,
asserting a bot's identity/health, or coordinating
multiple agents (chain-load identity guard: a shared
root context file that doubles as one bot's WD meta
must carry a top identity guard so other bots
chain-loading it don't absorb that identity; teammate
idle = drive not wait; meeting = adopt domain frames,
no solo lock)
└── meeting-protocol.md ← loaded only when coordinating an active meeting,
verifying a bot dispatch, writing KST progress rows,
or stopping while a meeting is still active
INDEX.mdis a compact trigger table:situation → rule file → one-line gist. It is the only thingCLAUDE.md/AGENTS.mdpoints to (one pointer block, not the rules themselves).- Each turn the agent scans INDEX's triggers; when the current situation matches a row, it Reads that one rule file then and applies it. No match → proceed. Rules are paid for only when relevant.
- Each rule file is focused and short (≈30 lines; split further if it grows). One file = one situation cluster.
- Rule files cross-link 1:1 with memory entries (
[[memory-slug]]). Memory = the incident/learning log (why); the rule file = the actionable spec (what to do). They are not duplicated. - Priority on conflict: explicit user instruction > rule file > inline default (same as superpowers instruction-priority).
Progressive disclosure: the router is tiny and always present, so the agent always knows that a rule may apply and where it is — but the heavy prose is loaded only in-context-of-use, when attention is already on that situation. Recall becomes a lookup, not a memorization test.
- Put the rule in the matching
rules/<topic>.md(new topic → new file). - Add or update one trigger row in
rules/INDEX.md. - If it came from a learned mistake, also write the memory entry and cross-link both ways.
- Keep
CLAUDE.md/AGENTS.mdpointing only atINDEX.md— never inline the rule back.
The bot's AGENTS.md (project-doc auto-loaded, see skill-portability.md §3) carries only the INDEX pointer. The rules/ dir lives in the bot WD (or a repo-tier path that travels with the persona). The same on-demand discipline applies: the bridge injects dynamic per-turn state; static rules stay in rules/ and are pulled by trigger — never re-injected per turn (mirrors the P1.5 trim lesson).
ThisCodex also ships optional hook helpers for active meetings:
hooks/bot-session-init.shis a SessionStart-compatible helper. It appends only generic active-meeting state and the progressiverules/INDEX.mdwhen they exist. Paths are derived fromMEETING_PROTOCOL_DIR,MEETING_ACTIVE_FILE,BOT_WD, orPWD; missing files are a graceful no-op.hooks/meeting-stop-reread.shis a Stop-compatible helper. It emits{"decision":"block","reason":...}(the only valid Stop block+inject primitive — the Stop event has no hookSpecificOutput variant) only for bot sessions with an active meeting file and a non-recursive Stop event. All other cases allow stop silently (empty stdout + exit 0).hooks/lib/hookkit.shis the shared fail-open helper library for the soft→hard gates.hooks/reply-gate.sh,hooks/completion-gate.sh,hooks/dispatch-verify.sh, andhooks/kst-timestamp.share Stop gates. They usedecision:blockto inject one more turn when the transcript shows a likely rule violation.hooks/automation-no-interactive.shandhooks/verify-before-push.share PreToolUse gates. They deny withhookSpecificOutput.permissionDecision="deny"andexit 0, matching the Codex 0.130+ contract covered by the test suite.hooks/meeting-liveness.pyis a standalone KST liveness checker for active meeting participants. It is dry-run by default and sends only with--send.
The helpers intentionally avoid maintainer-local vault paths and Discord thread IDs. A distribution can install them into its own hook runner, but the shipped contract remains path-parameterized and fail-open.
On Codex specifically, a wired Stop hook does not run until it is trusted
via the Codex /hooks flow (a trusted_hash must land in
~/.codex/config.toml); otherwise the meeting reread is silently inactive even
though ~/.codex/hooks.json is correct. See README §3.6 and the setup skill
for the trust step. Claude Code has no equivalent trust gate.
Codex wiring (~/.codex/hooks.json) is user-global by design, so every session
gets the hooks regardless of working directory. But most deployments of this
convention run the same personas under Claude Code too, and there the scope
rules differ asymmetrically: Claude Code walks up for instruction files
(CLAUDE.md, front-loaded rules) but not for settings hooks. A Claude
session whose WD is a subdirectory with its own .claude/settings.json loads
none of the parent project's hooks — silently, while the instruction layer
keeps working and masks the gap. Measured in production (multi-bot vault,
2026-08-04, n=6 subdirectory-WD bot sessions): hooks registered only in the
project root had never fired in any of them (one such hook logged 2,064
injections in root-WD sessions against 0; a second logged 287 against 0 across
all six). Rule of thumb: mandatory Claude-side hooks go in
~/.claude/settings.json (user-global), with a cwd guard wrapper scoping them
to the workspace. Match the root with two patterns —
case "$CWD" in "$ROOT"|"$ROOT"/*) — so a single prefix glob cannot match a
sibling directory. Extract the cwd with jq -r '.cwd // empty': a bare .cwd
yields the string "null", which silently disarms the guard everywhere. See
the ThisCode companion's docs/rules-system.md section "Hook scope: settings
do NOT walk up" for the full shipped pattern, and verify a migration by a
natural firing inside a real subdirectory-WD session — a direct wrapper
smoke test cannot prove the settings-loading layer.
When one deployment serves both Claude Code (auto-loads CLAUDE.md) and Codex-style agents (auto-load AGENTS.md), do not maintain two rule bodies:
AGENTS.md= the shared operational rules (SSOT) — progressive-disclosure pointer torules/INDEX.md, completion gates, core paths.CLAUDE.md=@AGENTS.mdimport at the top + Claude-specific blocks only (identity guard, tool wrappers).
Caveats learned in production (obsidian-ai-vault charness adoption, 2026-06-11; items 3–5 from the 2026-08-05 mirror-staleness incident):
- A whitelist-style
.gitignore(*then!exceptions) silently untracks a new root file — add an explicit!/AGENTS.md. - Verifying that a Codex agent actually receives the chain: echo-style probes get contaminated — the injected rules change the probe's own behavior. Use session-transcript forensics (inspect the injected payload in the agent's rollout log) with a read-only sandbox instead.
- Make the chain visible at the top of every per-bot WD instruction file. A Codex bot's WD
AGENTS.mdreceives the shared rules via the 3-chain (~/.codex/AGENTS.md→ git-rootAGENTS.md→ WD), but anyone auditing just the WD file cannot see that — which reads as "this bot has no shared rules." Document the chain in the file's opening lines (the shipped bot templates model this). Never paste the shared body into the WD file — the chain already delivers it; a copy forks the SSOT. - Mirrors of live instruction files must be script-generated, never hand-copied. If
AGENTS.md/CLAUDE.md/persona files are mirrored into a human-readable wiki for owner review, the mirror must be produced by a maintained sync script — verbatim content plus anAUTO-MIRRORheader naming the origin path. Orphan hand-made copies rot silently: in production, six hand-copied mirrors froze for 5+ weeks and the owner audited stale rules and model names as if they were current. Register every new bot's files in the sync script's mirror list as part of bot creation. - Derived surfaces quote exact launch flags and name the launch SoT. Model/effort mentions in bot docs must carry the real launch arguments plus where they are launched from (shell alias, launchd plist, launch script). Alias-form snapshots go stale silently when the alias retargets to a new model generation.
The progressive-disclosure pattern above relies on the agent scanning INDEX each turn and Reading the matching rule. That scan is soft — under load, time pressure, or a long message chain the agent can skip it, leaving the rule unapplied ("knowledge ≠ enforcement"). Two optional, fail-open hook layers harden the high-cost rules without re-bloating always-on context:
Rather than trusting the agent to scan INDEX, a UserPromptSubmit hook reads the prompt, matches task-type keywords, and injects the matching rule's core gate into context right then. The mapping mirrors INDEX's trigger table (single source — the hook keys off task-types, it does not duplicate the rule bodies). This is complementary to a static always-on self-check, which becomes tuned-out precisely because it is identical every turn; the router is situation-matched, so it is relevant and hard to ignore. fail-open: no keyword match, or no jq/empty prompt → emit nothing, exit 0. (Codex: a UserPromptSubmit-equivalent hook must be trusted via the Codex /hooks flow before it runs — see setup skill.)
Where a rule maps to a detectable action (a tool call, a Stop, an outbound deliverable), a Stop or PreToolUse hook checks for the violation signal and blocks with a one-line reminder (Stop blocks via {"decision":"block","reason":...}). Shipped examples: completion-gate.sh (pre-report before declaring "done" to the user), dispatch-verify.sh (verify a delegated bot actually started before ending the turn), reply-gate.sh (deliver via the channel tool, not terminal-only). copy-coherence-gate.sh is the routing (non-blocking) variant: dual-anchored on PostToolUse(Write|Edit|MultiEdit) + PreToolUse(Bash git commit), it detects fact-document changes and injects a copy-sweep instruction. Two delivery contracts matter here, both measured: a Stop hook blocks only via {"decision":"block","reason":...} JSON, and a hook's plain stdout is NOT injected into the model context (measured on UserPromptSubmit; the {"hookSpecificOutput":{"hookEventName":...,"additionalContext":...}} JSON path is measured delivered on PostToolUse — positive and negative probes) — emit the JSON envelope or the model never sees it. Verify hooks at three layers: registered → fired (log line) → delivered (the agent's behavior changes); the first two without the third is a dead gate wearing a green light.
- Put hooks only on costly, repeatedly-skipped rules — never on the soft judgment/draft space (over-hooking = false positives = the agent fights its own harness). Mirror the autonomy gradient: read/draft = free, commit = self-judge, deploy/public = multi-party.
- fail-open is non-negotiable (uncertainty → allow): a buggy gate degrades to a no-op, never a session trap. Recursion-guard every Stop gate (
stop_hook_active). - Ship each gate with a block-case + allow-case test before wiring.
- Transcript-scanning Stop gates miss events summarized away by context compaction → carry an escape clause ("if already satisfied / not applicable, ignore and stop"); detection should key off filenames/structured fields, not free-text keywords (which false-fire on progress reports).
Origin: obsidian-ai-vault 2026-06-29 — a soft INDEX self-check was being skipped under load; rule-router (Layer 1) + action gates (Layer 2) were added. The router is the broad fix ("rules not read"); action gates are the targeted fix ("rule read but not obeyed").