Research, 2026-10-09. Not applied as a whole; the parts in use are described in
docs/routing.mdand AGENTS.md.
Research date 2026-10-09, Claude Code 2.1.295. Read-only: nothing in the repo was changed.
(Plan mode was active, so this report is written to the plan file. The requested path, …-agent-context-boundaries.md, was not writable.)
Builds on the earlier report (…-agent-abff93c9579131301.md, now also ideas/agent-context-setup.md). Only new or re-verified facts are repeated here.
Labels: [V] verified against a source listed in §9 (fetched 2026-10-09). [O] my opinion or design. [?] unverified, test before relying on it.
1. The problem in one picture
written for read by binds?
AGENTS.md agents + humans -> every session yes, but mixed with explanation
-docs/*.md humans + agents -> agent when nearby "decisions.md wins" (prose rule)
DocKindBanner agents (!) -> humans shows binding talk to people
handovers agents -> humans in Docs nav WIP shown as docs
ideas/drafts nobody decided -> agent reads them become decision recordsThree different things are mixed: what an agent must obey, what a person wants to understand, and what nobody has decided yet. Today a file's location doesn't tell you which of the three it is, so both readers guess. [O]
Main idea [O]: decide by where a file is, not by what it says. Claude Code tooling (rule paths, permission rules, hook if) can only select paths, never frontmatter or status fields [V: S1 "paths is the only frontmatter field"; S2 if takes one permission rule; S3 rules match file_path]. So the lifecycle state has to be part of the path, or live in typed data that a generator filters.
2. Verified mechanics this pattern relies on (new or re-checked)
| Need | Mechanism | Exact behaviour | Source |
|---|---|---|---|
| Hard rule for one area | .claude/rules/*.md with paths: | Loads when Read/Write/Edit (or cat/head on one file) touches a match. No precedence: "Claude may pick one arbitrarily" when two conflict | S1 [V] |
| Keep a folder out of reach | permissions.deny / ask with Read(...) | Applies to Read and Edit tools, @file mentions, IDE context, recognised Bash file commands (cat, head, tail, sed, tee) and redirects. Grep/Glob: "best-effort", rules applied to the searched directory. Not covered: grep -r pattern . run from a parent directory, and scripts that open files themselves. OS-level blocking needs the sandbox | S3 [V] |
| Hard OS-level read block | sandbox.filesystem.denyRead | Applies to Bash and its child processes only. "A denyRead entry doesn't stop the Read tool": pair it with a Read deny | S4 [V] |
No .claudeignore | none | "If your project has a .claudeignore file, it has no effect, so move its entries into Read deny rules" | S3 [V] |
| Skip instruction files | claudeMdExcludes (glob against absolute paths, any settings layer, arrays merge) | Excludes CLAUDE.md / rules files only. It doesn't hide ordinary docs | S1 [V] |
| Block writes to a path | Edit(...) deny/ask (covers Write too; Write(...) rules are never consulted) | Order deny → ask → allow. A PreToolUse hook's "allow" can't override a deny/ask rule | S3 [V] |
| Block with your own logic | PreToolUse hook, exit 2 or {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"…"}} | Can read env vars and files, so it can know a "focus" | S2 [V] |
| Procedure only the user triggers | skill with disable-model-invocation: true | Description not in context, only /name runs it | S5 [V] |
| See what loaded | /context (Memory files at launch), /memory (files, auto-memory toggle), Loaded <path> line for nested files, InstructionsLoaded hook | Hook input: file_path, memory_type, load_reason (session_start, nested_traversal, path_glob_match, include, compact), globs, trigger_file_path, parent_file_path. Can't block; async, observability only | S1, S2 [V] |
| Auto memory as a bleed channel | autoMemoryEnabled | Already false in the user's ~/.claude/settings.json (checked). Keep it off: it would carry lessons from a draft session into every later session | S1 [V] |
| Plugin skills | skillOverrides doesn't affect plugin skills; manage them with /plugin | Note: the installed mattpocock-skills:domain-modeling description says "record an architectural decision": a likely source of unprompted decision records | S5 [V] (cause: [O]) |
Practice sources:
- agents.md: "README.md files are for humans… AGENTS.md… the extra, sometimes detailed context coding agents need"; "The closest AGENTS.md to the edited file wins"; "explicit user chat prompts override everything" (S6 [V], undated live page).
- Anthropic, context engineering: "the smallest possible set of high-signal tokens"; just-in-time context; context rot (S7, 2025-09-29 [V]).
- MADR 4.0.0 (2024-09-17):
status: proposed | rejected | accepted | deprecated | … | superseded by ADR-0123(S8 [V]). - Diátaxis: four forms on two axes, action/cognition × acquisition/application (S9 [V]).
- llms.txt (Jeremy Howard, 2024-09-03): an H1, a blockquote, H2 link lists, an "Optional" section, and
.mdtwins of pages (S10 [V]). It targets agents reading a website. Agents here read the repo, so it isn't needed [O]. - Secondhand only [?]: Osmani's "≤150 lines; if AGENTS.md is longer than README it's too long"; an "ETH Zurich study" claiming README-style AGENTS.md hurts agents by 2–3%. Both were found only through blog summaries (S11), not verified.
3. The pattern: four zones, one direction of flow [O]
IDEAS (drafts) PROPOSALS DECIDED AGENT CONTEXT
ideas/** -> proposals (data) -> -lib/decisions.ts ==> .claude/rules/generated/*.md
agent: asks first agent: reads, -lib/rules/*.ts AGENTS.md (core)
human: Docs › Ideas "not adopted" rule agent: binding .claude/rules/*.md (hand-written)
human: Docs › Proposals human: illustrated never in the docs nav
pages (only via the context map)
promotion is a human act: /decide (user-only skill) + Edit "ask" on decided files| Zone | Folder / glob | Who writes | Agent sees it | Human sees it |
|---|---|---|---|---|
| A. Agent context (hard rules) | AGENTS.md, CLAUDE.md (@AGENTS.md), .claude/rules/**/*.md, .claude/rules/generated/**, .claude/skills/**, .claude/settings.json | human, or generated from D | always, or on matching path | only through the context map (§6), never as prose pages |
| B. Human docs | src/routes/*/*/-docs/** (prose), src/routes/*/*/_docs/** (pages), src/routes/docs/**, docs/*.md | human (agent on request) | as reference when touched; never binding | the docs site |
| C. Ideas / drafts | ideas/<slug>.md or ideas/<experiment-id>/<slug>.md (repo root, outside src) | human, or agent when asked | only when the user names it: Read and Edit set to ask | Docs › Ideas, marked "Draft" |
| D. Decided records | src/routes/*/*/-lib/decisions.ts, src/routes/*/*/-lib/rules/*.ts (machines), src/shared/terms/*.ts (decided terms) | human via /decide; edits ask first | through generated rules (decided items only) plus the TS itself | illustrated pages built from the same data |
Proposals stay where they are (src/shared/docs/proposals/*.ts). A rule scoped to that path says "under discussion, not adopted". They are a sub-state between C and D that the team owns.
Why typed data for D [O]: one record has two short texts, a human headline and an agent rule line, plus status. A generator gives the agent decided items only, as compact rule lines, scoped to the experiment's path. The human page renders headline, illustration and "why" with progressive disclosure. Superseded and open items never reach the agent as rules. This is what the XState charts already do for business rules, generalised.
3.1 Lifecycle, where the state lives
| State | Where | Agent treats as | Enforced by |
|---|---|---|---|
| idea, draft | ideas/** | nothing (asks before reading) | permissions.ask: Read(/ideas/**), Edit(/ideas/**) plus .claude/rules/ideas.md (paths ideas/**: "draft, binds nothing, don't turn into decisions, don't read other ideas") |
| proposal | src/shared/docs/proposals/** | input, not a rule | .claude/rules/proposals.md |
| open question | meta.question in machines; status: 'open' in decisions.ts | explicitly undecided: never invent an answer | generator emits open items as a "Don't decide these" list; already the repo's statechart rule |
| decided | decisions.ts status: 'decided', machines in -lib/rules/ | binding inside its experiment | generated rule file; bun test fails when the generated file is stale |
| superseded | same record, status: 'superseded', supersededBy: 'D5' | absent | the generator skips it; the human page shows it in a history timeline |
The folder separates draft from not-draft (tooling can see it). A typed field separates decided, open and superseded inside D (the generator and types can see it). Frontmatter status in markdown is used for display only, never for enforcement, because nothing in Claude Code reads it [V: S1].
3.2 Zones run inside silos
Zones A–D describe what kind of context a file is. Silos (§4) describe whose it is. Every file in zones C and D, and every generated rule, belongs to exactly one silo or to the shared tier. That is a core requirement, not an option.
4. Silos: one experiment, one context (core requirement)
The repo exists to hold conflicting approaches side by side. An agent working on prototype A must never read or apply prototype B's decisions, rules, drafts or notes. "Aligning" experiments is the failure mode, not a kindness. [user requirement]
4.1 Verified enforcement options
| Option | What it does | Verdict for silos | Source |
|---|---|---|---|
Path rules per experiment (paths: src/routes/prototypes/baseline/**) | B's rules load only when a B file is touched | Necessary, not sufficient: nothing stops the touch | S1 [V] |
Nested CLAUDE.md per experiment | Loads on the first Read/Write/Edit of a file in that folder | Same weakness: reading one B file pulls in all of B's instructions. Prefer central generated rules (visible to the context map) | S1 [V] |
permissions.deny / ask on sibling paths | Blocks Read/Edit tools, @ mentions, cat/head/tail/sed, redirects. Grep/Glob best-effort. Not grep -r from a parent dir or scripts | Primary mechanism. Applies to subagents too: "The rule applies to the main conversation and to subagents" | S3, S12 [V] |
claude --settings <file> | Adds a settings layer for this session, above project and user. Lists merge across layers, so a silo file adds deny rules to the project's | How focus is set: one generated settings file per silo | S13 [V] |
claudeMdExcludes in the same silo file | Skips sibling rule files by glob, even if a sibling file gets touched | Belt and braces for instruction bleed. Works "at any settings layer" [V]; that --settings counts as one is [?] (S13 says it "can set any key your user settings file can set") | S1, S13 |
PreToolUse hook with the silo id in its command (silo-guard.sh baseline) | Sees tool_input.file_path (always absolute) for Read/Edit/Write and path/pattern for Grep/Glob; for Bash, the command text. Exit 2 or permissionDecision: "deny" | Closes what permissions miss: grep -r/rg/find over src/routes, a Grep/Glob whose path is a parent of siblings | S2 [V] |
Launch from the experiment folder (cwd = silo) + blockReadsOutsideWorkingDirectories | File tools refuse reads outside the working dirs in every mode | Don't use. .claude/settings.json is read "from the session's primary working directory", so launching in a subfolder loses the project's hooks and permissions. Bash commands (bun run dev, lint) need the root | S3, S13 [V] |
Worktree (claude -w, worktree.sparsePaths) | Separate checkout; sparse paths check out only listed dirs | Physically removes siblings, but the router, lint (type-aware, whole repo) and typed links need every route. Fine for an idea worktree, wrong for experiment work. Note: worktree isolation checks guard edits into the main checkout, not reads | S14 [V]; sparse details [?] (index entry only) |
Sandbox filesystem.denyRead for siblings | OS-level, Bash and children only | Breaks bun run lint, tsr generate, vite, which read all routes. Hooks run outside the sandbox. Not recommended | S4 [V] |
Subagents (.claude/agents/<x>.md with hooks, disallowedTools, omitClaudeMd) | Per-agent hooks and tool lists. No per-agent path permissions | Useful for a one-off "work only in X" delegate (hooks with the silo id). Project agents' hooks run only after folder trust | S12 [V] |
| Import boundary check (Fallow boundaries, §4.4) | Fails when silo A imports silo B. Resolved graph, autoDiscover per folder | Code-level isolation, independent of agents. Today's edges: baseline→receipt, product-admin→receipt and image-grid, presentations→image-grid (14 imports, Fallow run). No prototype imports another prototype | repo + S15 [V] |
4.2 Recommended setup [O]
Tiers (each file belongs to exactly one, shown in the context map). The code boundary and the context boundary are the same line (§4.3):
| Tier | Paths | Holds product decisions? | Code may import it? | Agent in silo A |
|---|---|---|---|---|
| Project (how to work here) | AGENTS.md, .claude/rules/*.md (hand-written), scripts/**, config | no | n/a | reads; rules load as usual |
| Platform (neutral mechanisms) | src/platform/** (today's src/shared/{format,media,docs shell,statechart,theme,brand,page-meta,route-guard}, src/components/ui, src/lib/utils, src/styles), data/** | no, by definition | yes, every experiment | reads; Edit asks (a change reaches every silo) |
| Reference (never binding, never imported) | src/reference/{im-web,proposals,working-model}/** (today src/shared/docs/im-web, …/proposals, baseline's -lib/rules as working model), docs/im-web/** | describes or proposes them | no: only src/routes/docs/** renders it; experiments copy from it | reads, as input; a rule per folder says what it is |
Silo <id> | src/routes/<kind>/<id>/**, ideas/<id>/**, .claude/rules/generated/<id>.md | yes, its own | only by itself | own silo: everything. Other silos: deny Read (no uses exceptions once §4.3 is done) |
| Unsiloed ideas | ideas/_inbox/** | not yet | no | ask |
The working model moves from "baseline's rules that others may follow" to a reference folder that every experiment, baseline included, copies from. Departing from it is then a visible diff plus a decision record, not a silent edit to shared code. [O]
How a session gets a silo:
bun run focus baseline
-> scripts/focus.ts reads registry.ts and writes .claude/silos/baseline.json (gitignored, regenerated each time)
-> exec claude --settings .claude/silos/baseline.json -n "baseline".claude/silos/baseline.json (generated):
{
"permissions": {
"deny": [
"Read(/src/routes/prototypes/product-admin/-docs/**)",
"Read(/src/routes/prototypes/product-admin/-lib/decisions.ts)",
"Read(/src/routes/prototypes/product-admin/-lib/rules/**)",
"Read(/ideas/product-admin/**)"
// ... per sibling; with §4.3 done, simply "Read(/src/routes/prototypes/product-admin/**)"
// Read deny also blocks Edit and Write (S3)
],
"ask": ["Edit(/src/platform/**)", "Edit(/src/reference/**)"]
},
"claudeMdExcludes": ["**/.claude/rules/generated/product-admin.md", "..."],
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Grep|Glob",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/silo-guard.sh baseline"
}
]
}
],
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/silo-intro.sh baseline"
}
]
}
]
},
"statusLine": {
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/silo-status.sh baseline"
}
}silo-guard.sh <id>denies Grep/Glob whosepathis a parent of sibling silos (e.g.src/routes), with the reason "search inside src/routes/prototypes/baseline or src/shared". It also denies Bashgrep -r/rg/find/catnaming sibling silo paths or recursing oversrc/routes. It is best-effort for Bash (S2 says so forif). The script parses the command itself.silo-intro.shprints SessionStartadditionalContextas facts: "This session works on Baseline (prototype). Shared: project rules, working model, reference. Other experiments are closed in this session." (S2 [V]additionalContext.)- The status line shows
◆ baselineso the human always sees which silo the session is in. - The silo id is baked into hook commands, so nothing depends on env-var inheritance [?].
- Leaving a silo means ending the session. Settings from
--settingsare fixed for the session. Cross-silo work ("compare baseline and product-admin") is a deliberate unfocused session started with plainclaude, where the generated rules still scope each silo's decisions to its own paths. - Delegation: subagents inherit the deny rules (S12 [V]). Explore/Plan skip CLAUDE.md but not permissions.
- A silo is a folder. Fallow's
autoDiscoverturns every child ofsrc/routes/prototypesandsrc/routes/featuresinto a zone (§4.4).scripts/focus.tsreads the zones fromfallow list --boundaries -f json(verified output: zone names, patterns, file counts, rules), and the registry adds titles. Silo settings, generated rules and the context map share that one definition, so a new experiment folder is siloed automatically.
Checks that keep the silos true:
- Import boundary:
fallow dead-code --boundary-violationswith the config in §4.4. It works on the resolved module graph (it caught the@/alias imports; relative climbs resolve the same way), so no separate import-graph test is needed. It runs per edited file in a PostToolUse hook and in full at commit. - Zone coverage:
boundaries.coverage.requireAllFilesreports any source file outside every zone, so a new top-level folder can't slip past the silo map. - Generated rule paths for
<id>never match a file outside<id>. - No hand-written rule in
.claude/rules/haspathsreaching into a single experiment (those belong in its generated rule). - No
-docs/*.mdnames another experiment's decision IDs ("as product-admin D12") except in a declared comparison doc.
4.3 Code isolation: experiments import only the platform [O, evaluated against the repo]
The main thread's proposal: experiments never import other experiments, they copy. They import only a neutral platform layer with no product decisions. Anything shared that carries a product decision moves into an experiment. Proposed enforcement: no-restricted-imports overrides generated per experiment folder in oxlint.config.ts. Recommended instead: Fallow boundaries (§4.4), which need no generated config and check resolved imports.
Verdict: adopt it. Don't go further to full self-containment (zero imports outside the folder).
| Option | Gain | Cost | Verdict |
|---|---|---|---|
| Status quo (prototypes may import features) | No copies | Silo A's agent must read B's code (uses exceptions); a fix in receipt silently changes baseline's checkout; context and code boundaries differ | No |
| Platform-only imports (proposal) | Code boundary = context boundary; permissions become "deny every sibling"; each experiment can change anything it owns | 3 dependency edges to cut: baseline→receipt, product-admin→receipt+image-grid, presentations→image-grid (verified: no prototype imports a prototype; src/routes/docs imports no experiment) | Yes |
| Full self-containment (copy shadcn, format, docs shell into each) | Total independence | components/ui alone has 215 imports across experiments, shared/format 81, shared/docs 79 (counted). Copies of mechanisms with no product meaning drift for no benefit, and the docs site loses one look | No: the copies would carry no decisions, so there's nothing to isolate |
The test for "platform": could two experiments reasonably want it to behave differently as a product question? If yes, it's not platform. [O]
| Today | Product decision inside? | Goes to |
|---|---|---|
components/ui, lib/utils, styles, shared/theme, brand | no (vendored, look of the live site) | platform |
shared/format.ts | FORMAT_LOCALE = 'nb-NO' is a project convention (baseline D1 restates it). Price display rules (rounding, "incl. VAT") are product decisions | platform keeps formatters with a locale parameter; rounding and VAT wording stay in experiments |
shared/media (types, fixtures, preview) | no: mirrors NTB's API, must not change | platform |
shared/statechart, docs shell, page-meta, route-guard, docs nav | no (engines) | platform |
shared/prototypes (registry, prototype menu, app header) | registry = project metadata; app header = live-site chrome | platform (registry could sit under project) |
shared/docs/im-web, shared/docs/proposals, doc-kinds text | content, not mechanism | src/reference/ |
baseline -lib/rules as working model | yes | src/reference/working-model/, copied by each experiment |
features/receipt used by baseline and product-admin | yes (seller, VAT lines, layout = R decisions) | copied into each prototype |
features/image-grid used by product-admin and presentations | mostly engine (virtualisation, tiles); its decisions are about the grid's look and behaviour | either graduate the engine to platform (src/platform/image-grid) and keep the feature as its docs and demos, or copy. Graduate if the engine is stable |
Naming and location: src/platform/ (flat: ui/, format/, media/, statechart/, docs/, theme/, brand/, routing/), with an @/platform/* alias. "Platform" says "what experiments stand on" and pairs with "reference" (what they read) and "routes" (what they are). shared says nothing about decisions, which is the property that matters. Keep shadcn where components.json aliases expect it, or repoint the aliases to src/platform/ui [?: check shadcn CLI with custom alias]. src/reference/ holds data and renderers for /docs only.
Lint fallback (only if Fallow isn't used). oxlint 1.87 supports overrides[].files and no-restricted-imports with patterns (group, regex), checked in node_modules/oxlint/dist/index.d.ts. A readdirSync in oxlint.config.ts can generate one override per experiment, with regex: ^@/(routes/(?!<kind>/<id>/)|reference/). Weaknesses: it matches the specifier text, not the resolved file, so it misses relative climbs (../../receipt/-lib/x) and import.meta.glob, and it needs regex escaping per folder. Fallow covers all of that with five lines of config. [O]
Copies and drift:
- Copy with a script, not by hand:
bun scripts/copy-from.ts features/receipt prototypes/baselinecopies intoprototypes/baseline/-lib/from-receipt/and writescopied-from.json(source path, commit hash, file hashes). This is the shadcn "copy in, own it" model. fallow dupesmeasures sameness, the inverse of drift. Run read-only on this repo:fallow dupes --skip-local --min-occurrences 2took about 0.1 s and found baseline/product-admin_checkout.tsxsharing 89 lines andpurchase-panel.tsxsharing 70 lines (product-admin started as a copy of baseline). Its output also caught order-flow'sboard/graph.tsandrouting.tsduplicatingsrc/shared/statechart, a platform engine copied into a feature. So for a copy pair, "shared lines falling over time" = divergence. Recordfallow dupes -f jsonper pair in the drift report. It can't say which side changed or whether the source moved since the copy; that needscopied-from.jsonplusgit diff <hash> -- <source>.- Fallow's default advice is wrong here: it suggests "Extract shared function … into a shared directory" for cross-experiment clones.
fallow explain code-duplicationitself says "Leave it duplicated when the similarity is accidental and likely to diverge", which is exactly the experiment case. Keep cross-experiment clones out of any gate (§4.4) and tell agents so in the experiments rule, or an agent will "fix" the silos by merging them. - Drift is reported, never auto-fixed. A docs block on the experiment's page (and a column in the context map) says "receipt changed in 3 files since baseline copied it at abc123". A human decides: re-copy, or keep the difference and record why.
- Risk: bug fixes don't propagate. Accept for experiments (each is a snapshot of an idea). For mechanisms that keep needing the same fix, the fix is a signal to graduate that code to platform.
- Risk: accidental differences look like decisions. Mitigation: a difference in a copied folder with no decision record citing it shows as "unexplained" in the drift report.
- Risk: platform turns into a back door (silo A edits platform and changes B). Mitigation:
Edit(/src/platform/**)isaskin silo sessions, a platform rule says "changes reach every experiment", and the context map shows platform as one band under all silos.
Agent view this produces: an agent in A sees A, the platform, read-only reference and project rules. Nothing else exists for it: siblings are denied, and nothing in A imports them, so following an import never leads out of the silo. That's why the code rule makes the context rule simple: one deny per sibling, no uses carve-outs.
4.4 Fallow for code silos (verified locally, read-only, Fallow 3.14.0)
What I ran (config in the scratchpad, --no-cache, git status clean afterwards): fallow config-schema, fallow list --boundaries, fallow dead-code --boundary-violations, fallow dupes, fallow explain, fallow hooks install --target agent --agent claude --dry-run.
Result with the config below: 10 zones (2 prototypes, 6 features, platform, site). Exactly 14 violations, all of them the three known edges: baseline→receipt (5), product-admin→receipt (5) and image-grid (2), presentations→image-grid (2). There were none from platform to routes and none from site to experiments. A single-file check (--file <path>) took 0.13 s, fast enough for a per-edit hook.
Proposed .fallowrc.json. It follows the format and settings of im-web's (.fallowrc.json at b802eaf): JSONC with comments, the same $schema, duplicates.minOccurrences: 3, rules.unused-dependencies: "warn", and ignoreDependencies with a reason per entry. There are no workspaces (this repo isn't a monorepo), and boundaries is added:
{
// This file is JSONC: fallow reads these comments; generic JSON tools may not.
"$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json",
"entry": ["src/router.tsx", "src/routes/**/*.{ts,tsx}", "scripts/*.ts"],
"duplicates": {
// As im-web: hide pair-only clones. Copies between experiments are pairs on
// purpose; drift is measured separately with `--min-occurrences 2` (see the drift report).
"minOccurrences": 3
},
"rules": {
"unused-dependencies": "warn"
},
"ignoreDependencies": [
// fill as findings show up, one comment per entry (im-web style)
],
"boundaries": {
"zones": [
// Every child folder is its own zone: prototype/baseline, feature/receipt, ...
{ "name": "prototype", "autoDiscover": ["src/routes/prototypes"] },
{ "name": "feature", "autoDiscover": ["src/routes/features"] },
// Neutral mechanisms, no product decisions (today src/shared + vendored UI; later src/platform).
{ "name": "platform", "patterns": ["src/shared/**", "src/components/**", "src/lib/**", "src/hooks/**", "src/styles/**", "src/styles.css"] },
// Reference content: im-web today, proposals, working model. Rendered by the site only.
{ "name": "reference", "patterns": ["src/reference/**"] },
// The site around experiments: root, start page, Docs, section pages, router.
{ "name": "site", "patterns": [
"src/routes/__root.tsx", "src/routes/index.tsx", "src/routes/docs/**", "src/routes/meta/**",
"src/routes/prototypes/{route,index}.tsx", "src/routes/features/{route,index}.tsx", "src/router.tsx"
] }
],
"rules": [
// An experiment imports itself (implicit) and the platform. Nothing else: copy instead.
{ "from": "prototype", "allow": ["platform"] },
{ "from": "feature", "allow": ["platform"] },
{ "from": "platform", "allow": [] },
{ "from": "reference", "allow": ["platform"] },
{ "from": "site", "allow": ["platform", "reference"] }
],
"coverage": {
"requireAllFiles": true,
// Generated route tree imports every route; config and scripts sit outside the app.
"allowUnmatched": ["src/routeTree.gen.ts", "*.config.ts", "scripts/**"]
}
}
}Notes:
- Autodiscovered children are verified. A rule on the group (
from: "prototype") expands to one rule per child (prototype/baseline → platform), and imports within one child are allowed. A sibling child is a violation (prototype/baseline -> feature/receipt). src/routeTree.gen.tsmust stay unzoned. In the first run it fell intositethrough asrc/*.tspattern and produced about 60 false violations. Unzoned files are unrestricted (schema: "unzoned files are unrestricted").allowUnmatchedpatterns for config files andsrc/routes/{prototypes,features}/{route,index}.tsxare inferred from the coverage report of the trial run. The brace pattern is [?]: the trial used plain lists.calls.forbiddencan carry silo-level bans with no product meaning, e.g.{ "from": "platform", "callee": ["fetch"] }, or bantoLocaleStringeverywhere (the Intl rule from AGENTS.md) [?: segment-aware matching of*.toLocaleString].- Presets (
layered,hexagonal,feature-sliced,bulletproof): none fits "siblings that never talk". Custom zones are simpler. [O] fallow vizwrites an interactive HTML module map (or--viz-format mermaid|dot). It is useful to see code silos, but it shows module graphs, not agent context. For the context map, readfallow list --boundaries -f jsoninstead (§6).
Fallow in the hook plan:
| When | Hook | What runs | Blocks? |
|---|---|---|---|
After each edit in src/** | PostToolUse Edit|Write, if: "Edit(src/**)" (next to oxc.sh) | fallow dead-code --boundary-violations --file "$FILE" -f compact --fail-on-issues | exit 2 with the violation, so Claude sees "prototype/baseline -> feature/receipt" immediately |
Before git commit / git push by the agent | Fallow's own agent gate: fallow hooks install --target agent --agent claude (dry run: adds 1 handler to .claude/settings.json and creates .claude/hooks/fallow-gate.sh). The script parses Bash commands for git commit/push, runs fallow audit, and blocks on a fail verdict. It needs jq and fails open on runtime errors | fallow audit (dead code, complexity, duplication, boundaries on changed files; gate: new-only by default) | yes |
| Before commit by a human | fallow hooks install --target git (pre-commit) | same audit | yes |
Two adjustments before installing the gate [O]:
- Duplication must not gate copies. A fresh
copy-fromcreates clones on purpose. Either runfallow dupes --save-baselineinsidescripts/copy-from.tsand pointaudit.dupesBaselineat it, or downgrade duplication in audit forsrc/routes/**. Otherwise the gate pushes the agent to extract shared code across silos, the exact bleed this pattern prevents. - Unused-code findings in experiments: route files and
-libexports used only by docs previews may show as unused. TuneentryandignoreExportsbefore gating, or start with--gate new-only(the default).
Codex variant: --agent codex appends a managed block to AGENTS.md, which would put tool text into the human-readable core file. Don't use it here, or keep it below a clear marker. [O]
5. Which tool for which kind of content (one source, two renderings) [O]
XState is one way to keep one source with two renderings. Pick the tool by the content's shape:
| Content | Source of truth | Human rendering | Agent rendering | Notes |
|---|---|---|---|---|
| Lifecycles, flows with states (pack, subscription, checkout, order flow) | XState ChartDefinition (exists) | interactive chart, scenarios, derived decision table | the machine TS; optionally a generated transition list (state, event, guard text, target, sender) in the experiment's generated rule | meta.question = open question, already derived. Keep it |
| Condition → outcome rules without state (price rounding, VAT basis, which badge for which plan) | decision table as data: rows: {when: {...}, then: {...}, note?}[] typed in TS | coloured grid, row highlight from a scenario picker | compact markdown table in the generated rule | Each row doubles as a bun test case against the real function: the table can't lie |
| Decisions (D1–D38, DS1–DS15, R1…) | -lib/decisions.ts: Decision[] (shape below) | cards grouped by topic: plain headline, real component preview or Figma ref, "Why" and sources collapsed, a supersession timeline | generated .claude/rules/generated/<id>.md: ID: rule line, decided only, plus an "Open, don't decide" list | Replaces decisions.md. Code paths and IDs go in the agent line, not the human headline |
| Terminology (im-web today, proposals, our decided words) | typed data (exists: im-web/terms.ts, proposals/terminology-*.ts); add src/shared/terms/decided.ts or per-experiment terms | glossary with no/en side by side, "we say / we don't say", example in context | generated "Use these words" table, scoped to UI paths (src/routes/*/*/app/**, -lib/components/**) | Only decided terms reach the agent; im-web terms and proposals stay reference |
| Design tokens, layout principles | tokens.ts + tokens.css (tested) | specimens, diagrams (exist) | the TS; a 5-line rule scoped to UI paths | Already single source plus test |
| Structure (routes, experiments) | route tree + staticData (exists), registry.ts | generated nav, section index, a tree diagram | type guards (route-guard.ts), short rule | A failing type check is the best agent doc |
| Status / what's built / next steps | status in data (registry, or -lib/status.ts with `built | planned | blocked`) or plain markdown | progress board or checklist |
| Explanations, ELI5, rationale, user-test findings, UX handovers copied as source | plain human prose (markdown in -docs/), with diagrams | the docs page | none: agents may read it when relevant but it never binds | This is where human-only prose is the right answer. No binding words in it (§7) |
| Flows that aren't rules (a user-test script, a system context, a sequence) | diagrams-as-code: Mermaid in markdown for hand-drawn ones; ELK (exists) only for charts derived from data | rendered diagram | not needed | Mermaid would be a new dependency for @tanstack/markdown rendering [?]. Don't hand-draw a diagram of something a machine already defines |
| Procedures (new experiment, handover, refresh fixtures, perf, stable copy) | skills (agent) + a short how-to page (human) | how-to page (Diátaxis "how-to") | SKILL.md | Two texts on purpose: the audiences differ completely |
| Validated data from outside TS (frontmatter of idea files, imported JSON) | Zod schema | form, table | z.toJSONSchema() if an agent needs the shape [?: check Zod version in repo] | Use only where runtime validation is needed. Plain TS types are enough for in-repo data |
| MDX | — | — | — | Not needed: markdown plus typed data plus real React pages already covers it, and MDX would mix prose and code in one file, which is the thing to avoid |
Decision record shape (sketch):
/** One decision in one experiment. Human page and agent rule are both built from this. */
export type Decision = {
id: `D${number}`
topic: 'Pricing' | 'Checkout' | 'Copy' | 'Layout' | ...
status: 'decided' | 'open' | 'superseded'
/** For people: one plain sentence, no IDs, no paths. */
headline: string
/** For agents: one imperative line, may name files and tokens. Required when decided. */
rule?: string
why?: string
sources?: { label: string; ref: string }[] // Jira key, Figma node, Slack date
date: string
supersedes?: Decision['id']; supersededBy?: Decision['id']
/** Narrower than the experiment, e.g. ['app/_checkout/**']. */
appliesTo?: string[]
preview?: () => ReactNode // the real component, in the human page only
}Two short texts per record is deliberate duplication. They sit side by side, so drift is visible in review. A test requires rule when status === 'decided' [O].
6. The context map: "What Claude reads, where" [O]
A human docs page at /docs/project/agent-context that replaces ideas/agent-context-setup.md as the visible account of agent context.
What it shows (simple first, details on demand):
- Silo view (first screen). One column per experiment (title and kind from the registry), with the platform, reference and project tiers as bands under all columns. Each column card lists what an agent in that silo gets: "Always: AGENTS.md · When touched: 3 rules + its decisions (12 decided, 3 open) · Closed: 7 other experiments". It also shows the code boundary (Fallow zone, files, violations, normally 0) and copies with drift ("copied receipt at abc123 · 89 % still identical"). The project band shows what applies everywhere. A red marker appears on any rule, permission or import that crosses a column: a leak.
- Matrix. Rows are areas named by their route
staticDatatitles: each experiment × {App, Docs pages, Code, Notes}, plus Shared, Ideas, Data. Columns are context items. A cell holds a dot in the item's colour. Legend (six kinds, one colour each): Always (AGENTS.md) · When touched (path rule, nested CLAUDE.md) · Generated from decisions · Asks first (ask) · Blocked (deny) · Checked after edit (hookif) · Procedure (skill, user-only or automatic). - Click a row: "When Claude works here it reads…", a list of cards with each rule's title and first line, full text behind a disclosure, and a link to the source file.
- Warnings: a rule whose globs match no files (stale), an area with no rules, two rules on one area that mention the same topic.
- Optional: "Seen in practice". An
InstructionsLoadedhook appends{file_path, load_reason, globs, trigger_file_path}to.claude/logs/instructions.jsonl(gitignored). The map compares declared rules with observed loads ("declared forapp/**, loaded 14 times this week").
How to build it here:
src/shared/agent-context/context.server.ts(createServerFn, per the repo's data-flow rule) reads:AGENTS.mdandCLAUDE.md, plus nestedCLAUDE.md/AGENTS.mdfound withgit ls-files.claude/rules/**/*.mdfrontmatterpaths(YAML), title = first#heading.claude/settings.jsonpermissions.deny/askand hooks withif.claude/skills/*/SKILL.mdfrontmatter (paths,disable-model-invocation)
- It matches globs against
git ls-fileswithBun.Globor picomatch. Caveat [V: S1/earlier report]: Claude's engine for rulepathsisn't named. Permissions use gitignore semantics. Keep globs plain (*,**,{a,b}) so every engine agrees. - Silos come from
fallow list --boundaries -f json(zones, patterns, file counts, rules) and violations fromfallow dead-code --boundary-violations -f json, run by the server function or abun run context-mapscript that writes a JSON snapshot (Fallow is a global install, so the page must not require it at runtime: fall back to the snapshot). - Leak detection: a rule whose
pathsmatch files in two silos; a silo settings file that doesn't deny some sibling; a generated rule outside its silo; a Fallow violation. - It names areas from the router's route tree (the same walk as
docs-nav-tree.ts) plusregistry.ts, so people see "Baseline › App", notsrc/routes/prototypes/baseline/app/**. bun test: every rule glob matches at least one file; every generated rule is up to date; every experiment in the registry has a generated rule if it has decisions.- The page is about agents but written for people: it shows what applies where. It isn't agent context itself and uses no binding words.
7. Hard rules for agents (few) and how each is enforced
| # | Rule (agent wording) | Enforcement | Strength |
|---|---|---|---|
| 1 | Only decided records bind: generated rules, AGENTS.md, .claude/rules/. Everything else is reference. | Structure: only the generator writes decided items into agent context | deterministic (structure) |
| 2 | Don't read or edit ideas/ unless the user names the idea. | permissions.ask: Read(/ideas/**), Edit(/ideas/**); rule ideas.md loads on read with "draft, binds nothing" | deterministic except grep -r from a parent dir [V: S3]. Optionally sandbox.filesystem.denyRead: ["./ideas"] if the sandbox is on [?: path form] |
| 3 | Never create or change decisions, rules or agent instructions unless asked; use /decide. | permissions.ask: Edit(/AGENTS.md), Edit(/CLAUDE.md), Edit(/.claude/rules/**), Edit(**/-lib/decisions.ts); deny: Edit(/.claude/rules/generated/**) (only the generator writes there); /decide skill with disable-model-invocation: true | deterministic (ask = human approves each one) |
| 4 | Human docs are for people: plain words, no "binds", "rule for", "decision", "agent", "may depart on purpose". | Rule scoped to **/-docs/**, **/_docs/**, docs/**, src/shared/docs/**; a bun test that scans those files (and docs TS strings) for a short banned-phrase list | test = deterministic at commit; the rule guides writing |
| 5 | Stay inside one experiment per session; never read, apply or "align with" another experiment's decisions or code. Copy code only through copy-from. | Silo session (bun run focus <id>): sibling Read deny, claudeMdExcludes, silo-guard hook, status line (§4.2). Code: Fallow boundaries per edit and at commit (§4.4). Generated rules are path-scoped in unfocused sessions | deterministic in silo sessions (Bash best-effort); code boundary deterministic everywhere |
| 6 | Copies between experiments are intended; never extract shared code from experiments into src/platform or merge them unless asked. | Rule in experiments.md; Edit(/src/platform/**) = ask in silo sessions; duplication not gating src/routes/** (§4.4) | ask = deterministic |
| 7 | Open questions stay open: never answer a meta.question or an open decision on your own. | Generator lists them under "Don't decide"; a test fails if a decided item references an open one [O] | mostly instruction |
Everything else already in AGENTS.md (Intl, ports, fixtures, tokens) follows the earlier report: lint, tests, permissions and hooks.
8. Migration sketch (brief)
| Today | Becomes |
|---|---|
AGENTS.md (145 lines, includes "Kinds of docs: what binds you") | Core of about 60 lines (earlier report §3.8). The kinds-of-docs table becomes path rules (im-web-today.md, proposals.md, working-model.md, ideas.md), each loading only where it applies. Human explanations (structure tree, design sources narrative) move to docs/ pages |
DocKindBanner / doc-kinds.ts binding text | Drop binding from human pages. Keep label plus a fact: "im-web today · as of 7 Oct 2026, commit 3bedd2a", "Proposal · from Ferdinand, 7 Oct · under discussion", "Draft". The binding meaning moves to rules |
-docs/decisions.md (baseline, product-admin, design-system, page-builder, receipt, image-grid) | -lib/decisions.ts → human Decisions page (cards) plus .claude/rules/generated/<id>.md. Start with baseline as a pilot |
order-flow assumptions.md, open-questions.md | Open questions are already meta.question: derive the page. Assumptions become status: 'open' decisions |
status.md | Stays human (or data in the registry). Never in agent context. "Next steps" go to handovers or Jira |
docs/handovers/ | Session notes are agent context (continuity). Move to .claude/handovers/, listed by a SessionStart hook, not in the Docs nav. Human-facing "what's open" comes from status |
ideas/agent-context-setup.md | Replaced by the context map page plus a short human "How Claude works here" explanation |
ux-lip-1440.md (copied UX handover) | Stays as source material (human, unchanged). Decisions cite it by section |
registry.ts | Titles and status for silos (folders define the silos via Fallow autoDiscover). workingModel links point to src/reference/working-model |
src/shared/** | Split: mechanisms → src/platform/ (alias @/platform/*); src/shared/docs/im-web, …/proposals → src/reference/; baseline's working-model machines → src/reference/working-model/, copied back into baseline |
| baseline/product-admin → receipt, product-admin/presentations → image-grid | copy-from into each experiment (receipt), or graduate the image-grid engine to src/platform/image-grid. Afterwards Fallow reports 0 violations |
order-flow board/graph.ts, routing.ts (copies of shared/statechart, found by fallow dupes) | Leave them (the feature owns its board), or import the platform engine if they haven't diverged on purpose. Decide once and record it |
| no Fallow config | .fallowrc.json as in §4.4; PostToolUse boundary check; Fallow agent gate after the dupes adjustment |
update-im-web skill | Add disable-model-invocation: true (its description already says user-only) |
| Plugin skills that write ADRs (e.g. domain-modeling) | Disable for this project via /plugin, or rely on rule 3's ask |
Order: (1) .fallowrc.json with boundaries in report-only mode (shows the 14 known violations); (2) cut the three edges (copy receipt, decide image-grid), then make boundary violations blocking; (3) bun run focus <id> with silo settings, guard hook and status line; (4) permissions and the ideas/ folder; (5) banner cleanup and the banned-phrase test; (6) baseline decisions.ts plus generator, generated rule and human page; (7) context map with the silo view; (8) the platform/reference rename; (9) other experiments' decisions.
9. Risks and open questions
- Read
askfatigue onideas/. Mitigation: approve "for this session" when you open an idea, or start a silo session for the idea's experiment, which allowsideas/<id>/**[O]. - Silo sessions are only as strong as their launch. A plain
claudesession has no sibling denies. Mitigation: the status line shows "no silo" in amber; generated rules still scope decisions by path; a SessionStart hook in project settings could print "unfocused: start withbun run focus <id>for experiment work" [O]. - Grep/Glob on a parent folder (
src/routes): S3 applies Read denies "to that directory" best-effort. Whether results from denied subfolders are filtered is [?]. The silo-guard hook denies such searches outright. - Fallow is a global install, not in
package.json(same as im-web). CI and other machines need it, or pin it as a dev dependency. Fallow's agent gate fails open whenjqor fallow is missing. - Fallow's refactoring advice ("extract shared") conflicts with the copy policy. Keep it out of gates for
src/routes/**and say so in agent context. - Copy drift. Bug fixes don't propagate, and accidental differences look like decisions. The drift report (
copied-from.json+git diff+fallow dupes) makes both visible; humans decide. - Bypasses:
grep -rfrom a parent directory, and scripts, ignoreReadrules [V]. Only the sandbox closes this, and only for Bash. - Generated files drift. Mitigation: a test that regenerates and diffs; deny agent edits to
generated/. - Two texts per decision (headline, rule) can disagree. Mitigation: co-location and review. Accepted cost.
- Claude-only:
.claude/rules/and generated rules are invisible to Codex and other agents [V: S1]. Alternative: generate nestedAGENTS.mdper experiment (agents.md "closest wins"; Claude loads a subdirectory AGENTS.md on Read when there's no CLAUDE.md there [V: S1]). Check that the router ignores.mdinsrc/routes[?]. - Glob engine differences between rules (unnamed engine), permissions (gitignore) and the context map (Bun.Glob or picomatch). Keep globs simple;
$slug.tsxand_docsare safe, avoid(…)[V/earlier report]. .claude/protected paths: edits under.claude/may already prompt in some permission modes [?]. Test before relying on rule 3 alone.- Open: do ideas live at the root (
ideas/) or inside experiments (<id>/-ideas/)? Root gives one glob and keeps drafts away from code; per-experiment keeps context together. Recommendation: root, with an optionalideas/<experiment-id>/subfolder. - Open: should Docs › Ideas be visible to test users and the team? It is rendered with a "Draft" label; hide it from the start page.
- Open: the working model as
src/reference/working-model/copied into every experiment (recommended, §4.2) means baseline's copy can drift from the reference too. Is the reference updated from baseline (baseline = where the working model is developed) or the other way round? Pick one direction. - Open: image-grid: graduate the engine to platform, or copy it into product-admin and presentations?
- Same pattern for Obsidian [O]: folders by state (
Inbox/,Drafts/vsDecided/),Readask or deny on drafts in the vault's.claude/settings.json, auto memory off, a user-only/decideskill.
10. Sources (fetched 2026-10-09 unless dated)
- S1 https://code.claude.com/docs/en/memory: CLAUDE.md, rules
paths,claudeMdExcludes, AGENTS.md,/context,/memory, auto memory, InstructionsLoaded - S2 https://code.claude.com/docs/en/hooks: InstructionsLoaded fields, PreToolUse deny JSON,
if, SessionStartadditionalContext - S3 https://code.claude.com/docs/en/permissions: Read/Edit rules, best-effort Grep/Glob, Bash limits,
.claudeignorehas no effect, deny → ask → allow - S4 https://code.claude.com/docs/en/sandboxing:
sandbox.filesystem.denyReadcovers Bash only - S5 https://code.claude.com/docs/en/skills:
disable-model-invocation,paths,skillOverrides(not for plugin skills) - S6 https://agents.md/: README for humans, AGENTS.md for agents, closest file wins (undated)
- S7 https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents (2025-09-29)
- S8 https://adr.github.io/madr/ (MADR 4.0.0, 2024-09-17)
- S9 https://diataxis.fr/ and https://diataxis.fr/compass/
- S10 https://llmstxt.org/ (2024-09-03)
- S11 secondhand, not verified: https://addyosmani.com/agents/15-agents-md/, https://blog.buildbetter.ai/agents-md-complete-guide-for-engineering-teams-in-2026/, https://developer.upsun.com/posts/insights/why-your-readme-matters-more-than-ai-configuration-files, https://arxiv.org/pdf/2609.00252 (spec-driven development: an ADR in a wiki the agent never saw)
- S12 https://code.claude.com/docs/en/sub-agents: frontmatter (
hooks,disallowedTools,omitClaudeMd,isolation: worktree), settings deny rules apply to subagents, no per-agent path permissions - S13 https://code.claude.com/docs/en/settings:
--settingslayer above project and user, lists merge, project settings read from the primary working directory - S14 https://code.claude.com/docs/en/worktrees:
--worktree, isolation checks (edits and commands into the main checkout), what worktrees share;worktree.sparsePathsknown only from the settings-reference index - S15 Fallow 3.14.0, local CLI:
fallow config-schema,fallow explain boundary-violation|code-duplication,fallow hooks install --dry-run; gate script source via opensrc (fallow-rs/fallow3.14.0,crates/cli/src/setup_hooks/fallow-gate.sh); docs https://docs.fallow.tools - S16 im-web
.fallowrc.json: https://bitbucket.org/NTB-NO/im-web/src/b802eaf45b8b53a833e27cb13fef42855a0c08c5/.fallowrc.json - Earlier report (S1–S11 there):
…-agent-abff93c9579131301.md