Status: research from 2026-10-09, not applied yet. Kept here so it isn't lost; it may become its own doc-only feature and also be applied to im-web. The route shape it recommends (
_docs/, a reserved app folder) is in place, withapp/as a URL segment instead of_app/(seedocs/routing.md).
Research date: 2026-10-09. Installed Claude Code: 2.1.295 (changelog entry dated 2026-10-08), so the docs below match the version in use. Read-only research: nothing in the repo was changed. (Plan mode was active, so this report is written to the plan file instead of the scratchpad path that was asked for.)
0. Sources
| # | Source | Date | Used for |
|---|---|---|---|
| S1 | https://code.claude.com/docs/en/memory ("How Claude remembers your project") | live docs, fetched 2026-10-09; mentions versions up to v2.1.285 | CLAUDE.md, AGENTS.md, imports, nested files, .claude/rules/ |
| S2 | https://code.claude.com/docs/en/hooks (reference) and /hooks-guide | live, fetched 2026-10-09 (33 events listed) | events, matcher, if, exit codes, output |
| S3 | https://code.claude.com/docs/en/skills | live, fetched 2026-10-09 | SKILL.md frontmatter, paths, progressive disclosure |
| S4 | https://code.claude.com/docs/en/features-overview ("Extend Claude Code") | live, fetched 2026-10-09 | CLAUDE.md vs rules vs skills vs hooks |
| S5 | https://code.claude.com/docs/en/large-codebases | live, fetched 2026-10-09 | per-directory CLAUDE.md vs path rules, deny rules |
| S6 | https://code.claude.com/docs/en/permissions (Read and Edit section) | live, fetched 2026-10-09 | path pattern syntax (also used by the hook if field) |
| S7 | https://code.claude.com/docs/en/best-practices | live, fetched 2026-10-09 | "Write an effective CLAUDE.md" |
| S8 | https://code.claude.com/docs/en/changelog (from anthropics/claude-code CHANGELOG.md) | top entry 2.1.295, 2026-10-08 | version dates for features |
| S9 | Blog: "Steering Claude Code: when to use CLAUDE.md, skills, hooks, and subagents", Michael Segner (Anthropic), claude.com/blog/steering-claude-code-skills-hooks-rules-subagents-and-more | 2026-06-18 | deterministic over prompted |
| S10 | Blog: "How Claude Code works in large codebases", claude.com/blog/how-claude-code-works-in-large-codebases-best-practices-and-where-to-start | 2026-05-14 | lean, layered CLAUDE.md |
| S11 | TanStack Router "File naming conventions", tanstack.com/router/latest/docs/framework/react/routing/file-naming-conventions | live, fetched 2026-10-09 (repo pins react-router 1.170.41) | _, (group), -, route.tsx |
Relevant changelog dates (S8): .claude/rules/ added 2.0.64 (2025-12-10); InstructionsLoaded hook 2.1.69 (2026-03-05); paths: accepts a YAML list 2.1.84 (2026-03-26); hook if field 2.1.85 (2026-03-26); if fixed for Read/Edit/Write path patterns 2.1.176 (2026-06-12); if: "Edit(src/**)" changed to cwd-anchored in v2.1.214; brace-expansion budget in paths 2.1.217 (2026-07-21); native AGENTS.md reading v2.1.277; /doctor prompt-audit v2.1.283; onFailure: "block" for hooks 2.1.295 (2026-10-08).
1. Verified from the docs
1.1 CLAUDE.md / AGENTS.md (S1, S4, S7, S9, S10)
- Size: "target under 200 lines per CLAUDE.md file. Longer files consume more context and reduce adherence." Startup shows a warning when a file is over the recommended length, and when the files together pass a combined limit ("Each CLAUDE.md, rules file, and
@pathimport counts as a separate file"). Files over 4 MiB are skipped. (S1) - What belongs there: "Keep it to facts Claude should hold in every session: build commands, conventions, project layout, 'always do X' rules. If an entry is a multi-step procedure or only matters for one part of the codebase, move it to a skill or a path-scoped rule instead." (S1). Best practices table (S7): include commands Claude can't guess, style rules that differ from defaults, repo etiquette, project-specific architecture decisions, env quirks, non-obvious gotchas. Exclude what Claude can read from the code, standard conventions, detailed API docs, frequently changing information, tutorials, file-by-file descriptions. Test for each line: "Would removing this cause Claude to make mistakes?"
- S10: "The root file should be pointers and critical gotchas only."
- Imports:
@path/to/file, relative to the importing file, up to four hops of recursion. Imports don't reduce context cost: "imported files also load at launch." Paths inside backticks or code blocks are not imported. Paths with spaces need\escapes. (S1) - HTML comments (
<!-- -->, block level) are stripped before injection: maintainer notes cost no tokens. (S1) - Load order: CLAUDE.md from cwd and every ancestor at launch, concatenated root-first. Subdirectory CLAUDE.md files load on demand, "once Claude reads, writes, or edits another file in that subdirectory" (Read tool, or a Bash
cat/headon one file). They don't show under/context› Memory files; aLoadedline appears instead. (S1) - After
/compact: the root CLAUDE.md is re-read from disk. Nested CLAUDE.md andpaths:rules reload on demand only when a matching file is touched again. (S1) - AGENTS.md (v2.1.277+): read natively only if there is no CLAUDE.md / .claude/CLAUDE.md / CLAUDE.local.md in cwd or above. With
CLAUDE.mdcontaining@AGENTS.md(this repo's setup), Claude reads CLAUDE.md with AGENTS.md through the import, never twice. The docs say to keep this setup. SubdirectoryAGENTS.mdfiles load on Read when that directory has no CLAUDE.md..claude/rules/files still load alongside. (S1) - Audits:
/doctorproposes trims of a checked-in CLAUDE.md (v2.1.206+)./doctor prompt-audit(v2.1.283+) finds stale, contradictory or outdated-model instructions across CLAUDE.md, AGENTS.md, rules and skills. (S1)
1.2 Path-scoped rules: .claude/rules/ (S1)
- Location:
.claude/rules/**/*.md, discovered recursively (subfolders are fine). User-level:~/.claude/rules/. - Without
paths: loaded at launch "with the same priority as.claude/CLAUDE.md" (so no saving over CLAUDE.md). - Exact syntax:
--- paths: - 'src/api/**/*.ts' --- # API Development Rules - All API endpoints must include input validationpathsis the only frontmatter field read ("any other field is ignored without an error"). It takes a YAML list or a comma-separated string. Brace expansion works ("src/**/*.{ts,tsx}"), with a budget of 1,000 expanded patterns per rule.[starts a bracket expression; escape a literal one as\[. If the YAML doesn't parse, the frontmatter is ignored and the rule loads unconditionally (seeclaude --debug). - When they load: "A path-scoped rule loads when Claude uses the Read, Write, or Edit tool on a matching file", or views one with a single-file Bash read (
cat,head). Symlinked paths match too. Glob examples in the docs:**/*.ts,src/**/*,*.md(= root only),src/components/*.tsx. - Precedence: none. Everything is concatenated. User rules come before project rules. "If two instructions contradict each other, Claude may pick one arbitrarily."
- Rules vs skills (S1 note): "Rules load into context every session or when matching files are opened. For task-specific instructions that don't need to be in context all the time, use skills."
- Per-directory CLAUDE.md vs path rule (S5): directory file = owned and versioned next to the code. Central path rule = "all conventions in one place, or the same rule applies to many scattered paths."
- Debugging: an
InstructionsLoadedhook logs which CLAUDE.md or rule file loaded and why (S1, S2).
1.3 Hooks (S2, S8)
- Events (33 in 2.1.295): SessionStart, Setup, UserPromptSubmit, UserPromptExpansion, PreToolUse, PermissionRequest, PermissionDenied, PostToolUse, PostToolUseFailure, PostToolBatch, Notification, MessageDisplay, SubagentStart, SubagentStop, TaskCreated, TaskCompleted, Stop, StopFailure, TeammateIdle, InstructionsLoaded, ConfigChange, CwdChanged, DirectoryAdded, FileChanged, WorktreeCreate, WorktreeRemove, PreCompact, PostCompact, PreModelSwitch, PostModelSwitch, Elicitation, ElicitationResult, SessionEnd.
- Handler types:
command,http,mcp_tool,prompt,agent(the last is experimental). Common fields:type,if,timeout,statusMessage,once(only honoured in skill frontmatter). New in 2.1.295:onFailure: "block"for command and http hooks. matcherfilters on the tool name only: "It can't filter on file paths.""Edit|Write"is an exact list. Any other character makes it an unanchored JS regex, soEdit.*also matchesNotebookEdit.- Native path filtering: yes, the handler-level
iffield (since 2.1.85, path patterns fixed in 2.1.176). It takes one permission rule, with no&&,||or lists. It is evaluated only on PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest and PermissionDenied. On any other event, a handler withifnever runs. Exact form:
Path semantics come from permission rules (S6), which use gitignore syntax:{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "if": "Edit(*.ts)", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/x.sh" } ] } ] } }*stays within one segment and**crosses directories. The anchors://abs,~/home,/path(relative to the settings source, which for project settings is the primary working directory), andpathor./path(relative to cwd). "In anifcondition for a file tool, a single-segment directory pattern like"Edit(src/**)"matches only thesrcdirectory in the working directory… To match a directory namedsrcat any depth, write"Edit(**/src/**)"." Bare filenames match at any depth. Parentheses need no escaping (Edit(./Finance (2024)/**)). For Bash, "Because theiffilter is best-effort, use the permission system rather than a hook to enforce a hard allow or deny." - Exit codes: 0 = success (stdout becomes context only for UserPromptSubmit, UserPromptExpansion, SessionStart and PostModelSwitch). 2 = blocking: PreToolUse blocks the call, and PostToolUse shows stderr to Claude. Any other code is non-blocking: "exit 1 does not block."
- PostToolUse feedback without blocking:
{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"..."}}, capped at 10,000 characters. Phrase it as facts, not imperatives. ${CLAUDE_PROJECT_DIR}is the project root. Handlers that are identical across settings files run once. All matching hooks run in parallel.
1.4 Skills (S3, S4, S5)
- Project skills:
.claude/skills/<name>/SKILL.md. Nested<subdir>/.claude/skills/load the first time Claude touches a file in that subdirectory. - Frontmatter:
name,description(recommended),when_to_use,argument-hint,arguments,disable-model-invocation,user-invocable,allowed-tools,disallowed-tools,model,effort,context: fork,agent,background,hooks,paths(globs, same format as rules),shell, plus the spec fieldsmetadata,license,compatibility. - Loading: the description is always in context (description plus
when_to_useare truncated at 1,536 characters, so put the key use first). The body loads on invocation and stays loaded. Withdisable-model-invocation: truethe description is not in context and only/nametriggers the skill.paths: "Claude loads the skill automatically only when working with files matching the patterns." Keep SKILL.md under 500 lines and put reference material in supporting files. - When to use (S3/S4): a skill when a CLAUDE.md section "has grown into a procedure rather than a fact", for reference material needed only sometimes, and for invocable workflows. Facts stay in CLAUDE.md. "Use
disable-model-invocation: truefor skills with side effects."
1.5 Deterministic enforcement (S1, S4, S9, S10)
- S1: CLAUDE.md is "context, not enforced configuration… To block an action regardless of what Claude decides, use a PreToolUse hook." "If the instruction is something that must run at a specific point… write it as a hook."
- S4: "Put guardrails in hooks. An instruction like 'never edit
.env'… is a request, not a guarantee… If a rule must hold every time, make it a hook rather than a prompt instruction." - S9 (2026-06-18): "A real guardrail needs to be deterministic, and the enforcement methods are hooks and permissions." "The model choosing to run a formatter is different from the formatter running automatically."
- S10 (2026-05-14): hooks for linting and formatting "enforce the rules deterministically". It names "Using prompts for things that should run automatically" as a common mistake. Review the configuration every 3 to 6 months and after model releases.
- S5/S6: for files that must not change, use
permissions.deny(Edit(...),Read(...)). AReaddeny also blocks Edit and Write.Write(...)path rules are never consulted; useEdit(...).
2. Inferred or unverified (check before relying on it)
- Which directory rule
pathsare relative to. The docs only show root-relative examples (src/api/**/*.ts). I infer that they're relative to the project root for root.claude/rules/. The docs don't say how nested.claude/rules/resolve. Keep all rules in the root.claude/rules/. - The glob engine for rule
pathsisn't named. The docs mention braces and[, but say nothing about(. Hookifand permissions use gitignore syntax, where(is literal. Engines such as picomatch can read(a|b)as a group, so a folder named(app)might match the folderappinstead. This risk is the main reason for the naming advice in §4. if: "Edit(...)"on Write calls. S6 says "Editrules apply to all built-in tools that edit files", and changelog 2.1.176 talks about "ifconditions for Read/Edit/Write tool paths", so I expectEdit(...)to match Write calls too. Test it once with a logging hook.- Timing of path rules on a new file. The rule loads on the Write that creates the file, so the first version of a new file may be written before Claude sees the rule. Hooks and lint catch that. Rules guide later edits.
CLAUDE.mdinsidesrc/routes/. I expect the TanStack router generator to ignore.mdfiles (it only picks up route-module extensions). Check withbunx tsr generateafter adding one._app/route.tsxas a pathless layout written as a directory. S11 documents_(pathless) and theroutetoken separately but never shows them combined. Verify with the generator before committing to it.- Wildcards in Bash
ifpatterns such asBash(*3200*)aren't documented. Do the check inside the script instead.
3. Applied to this repo
3.1 Current state
CLAUDE.md=@AGENTS.md(correct per S1).AGENTS.mdis 151 lines: under 200, but it loads in full every session, and about two thirds of it only matters for some paths (docs styling, statecharts, PDFs, ELK, themes, fixtures, design sources)..claude/settings.json: one PostToolUseEdit|Writehook,oxc.sh(oxlint --fix plus oxfmt, exit 2 with stderr on leftovers). This already follows S9 and S10..claude/skills/update-im-web/(52 lines). Its description says "Use ONLY when the user explicitly asks", but the skill is model-invocable, so its description sits in context every session.- No
.claude/rules/, no permissions, no other hooks. - Route layout is inconsistent for docs, which matters for globs:
- prototypes keep docs at a URL segment
docs/(src/routes/prototypes/<id>/docs/**) and the app in pathless_public/and_checkout/plusadmin/andmeta/ - image-grid, page-builder and receipt use a pathless
_docs.tsx+_docs/(which contains adocs/segment) - order-flow uses
docs/ - design-system keeps its docs pages at the feature root (just moved out of
_docs/) - presentations has no docs
- prototypes keep docs at a URL segment
3.2 Target: what goes where
| Content now in AGENTS.md | Goes to | Why |
|---|---|---|
| Purpose (3 lines), commands, ports, the two experiment kinds, kinds of docs and what binds, "decisions.md wins", one folder per experiment, internal consistency, self-contained repo, how to start and end a session (one line each pointing at skills), Related repos (2 lines) | AGENTS.md (core, about 50 to 70 lines) | facts needed in every session; cross-cutting; shared with non-Claude agents |
| Structure tree | cut, or keep 5 lines | S7: "Anything Claude can figure out by reading code" |
| Per-area rules and gotchas | .claude/rules/*.md with paths (§3.3) | load only when relevant |
| Procedures (new experiment, handover, refresh fixtures, stable copy, perf run, im-web update) | skills (§3.6) | "procedures belong in skills" |
| "never change X", ports, format and lint, tokens in sync | permissions, hooks, tests, lint (§3.4, §3.5, §3.7) | deterministic |
| Design sources (Figma IDs, Carbon, ntb.no measurements) | rule scoped to UI and design-system paths, or a reference skill | only needed for UI or design work |
Each experiment's -docs/decisions.md | per-experiment nested CLAUDE.md containing @-docs/decisions.md (§3.3b) | loads automatically on first touch of that experiment; decisions files are 15 to 84 lines |
Note: .claude/rules/ and nested CLAUDE.md are Claude-only. Codex and other agents only see AGENTS.md, so anything those agents must know stays in AGENTS.md. Because rules can only be scoped by path (no other frontmatter field), the split follows the folder layout.
3.3 Path-scoped rules (proposed files, globs from the actual tree)
All files go in the root .claude/rules/. The globs are written relative to the repo root.
| Rule file | paths | Content moved from AGENTS.md |
|---|---|---|
experiments.md | src/routes/prototypes/**, src/routes/features/** | One folder per experiment, what may be shared, prototypes may import features. Read this experiment's -docs/decisions.md first (backup for §3.3b). - prefix = ignored by router. Route files carry full paths. Link paths have no trailing slash. Registry entry in src/platform/prototypes/registry.ts |
data-flow.md | src/routes/**/*.{ts,tsx}, src/platform/**/*.ts | loaders call createServerFn; mutations then router.invalidate({ sync: true }); .validator not .inputValidator; isomorphic loaders, file access only in *.server.ts or server fns; serializable returns, JsonValue[]; per-prototype db.server.ts keyed by PROTOTYPE_ID; no TanStack Query; load the TanStack skill first |
public-site-look.md | src/routes/prototypes/*/_public/**, src/routes/prototypes/*/_public.tsx, src/routes/prototypes/*/_checkout/**, src/routes/prototypes/*/_checkout.tsx (with §4: src/routes/prototypes/*/_app/**) | copy the live public site; one colour mode per page; measured sizes (header 64, toolbar 56, rows 200/8, 768/400) |
statecharts.md | src/routes/*/*/-lib/rules/**, src/platform/statechart/** | ChartDefinition; meta.label, meta: { label, sender }; meta.question; guardText; time as events, not after; derived decision tables; ELK lazy in effects; canvas fit threshold |
docs-pages.md | src/routes/docs/**, src/routes/*/*/docs/**, src/routes/*/*/_docs/**, src/routes/*/*/_docs.tsx, src/platform/docs/**, src/routes/*/*/-docs/** | DocsShell, DocsPage, DocKindBanner; DS tokens without dark:; .ds-light/.ds-dark; Components pages (h2 per variation, "what to check", data-toc-ignore); visual first; every doc viewable in the app; layout principles (sections, containers, rows, cells, 16 px) |
im-web-today.md | docs/im-web/**, src/reference/im-web/**, src/routes/docs/im-web/** | describes, never prescribes; never "correct" it to match an experiment; change only through /update-im-web |
proposals.md | src/reference/proposals/**, src/routes/docs/proposals/** | not adopted until moved into the working model or a decisions.md |
design-system.md | src/routes/features/design-system/**, src/styles.css, src/platform/theme/** | Carbon as reference, ntb.no as visual source, DS decisions, tokens.ts and tokens.css together, Geist override |
themes-vendored.md | src/styles/**, src/platform/ui/**, src/platform/ui/use-mobile.ts | copied from im-web or shadcn; lint ignores them; don't hand-edit |
media.md | src/platform/media/**, data/**, scripts/ingest-server.ts | MediaObject mirrors NTB search API; refresh steps go in the skill |
pdf.md | **/*.pdf.ts, src/routes/features/receipt/-lib/pdf.server.ts | playwright-core channel: 'chrome', one browser, localhost not tunnel |
vite-config.md | vite.config.ts | devtools console piping stays off (froze Chrome); Vite 8 log forwarding; restart after bun add and rm -rf node_modules/.vite |
handovers.md | docs/handovers/** | format, pruning |
formatting.md | src/**/*.{ts,tsx} | Intl through src/platform/format.ts; JSDoc on new modules (short: lint enforces most of it) |
3.3b Per-experiment nested CLAUDE.md (recommended in addition to experiments.md):
src/routes/prototypes/baseline/CLAUDE.md containing:
@-docs/decisions.md
@-docs/status.mdLoads the first time Claude reads or edits any file in that experiment (S1). It enforces "the experiment's decisions.md wins" without a hand-written instruction, and costs nothing in other sessions. scripts/new-prototype.ts would copy it along. Check item 5 in §2 (router ignores .md) first.
3.4 Permissions (deterministic "never change")
Add to .claude/settings.json. Use a leading / (anchored at the project root for project settings), because a single-segment deny such as Edit(data/**) would match any data/ folder at any depth (S6):
"permissions": {
"deny": [
"Edit(/data/**)",
"Edit(/src/routeTree.gen.ts)"
],
"ask": [
"Edit(/src/platform/media/types.ts)",
"Edit(/src/styles/**)",
"Edit(/src/platform/ui/**)",
"Edit(/src/platform/brand/**)",
"Edit(/docs/im-web/**)",
"Edit(/src/reference/im-web/**)"
]
}(ask instead of deny for vendored or im-web files, because the shadcn CLI and /update-im-web legitimately change them.)
3.5 Hooks
Keep oxc.sh. Add:
| Hook | Event and filter | Replaces AGENTS.md text |
|---|---|---|
tokens-check.sh: run bun test src/routes/features/design-system/-lib/tokens.test.ts, exit 2 with output on failure | PostToolUse, matcher Edit|Write, "if": "Edit(src/routes/features/design-system/-lib/tokens.*)" | "Change tokens.ts and tokens.css together" |
no-local-paths.sh: grep the edited file for /Users/, /home/, ~/, /private/tmp; exit 2 | PostToolUse Edit|Write (one handler per area via if, or filter inside the script) | "Never reference files outside it… absolute paths" |
guard-port.sh: block Bash commands that bind or kill port 3200 | PreToolUse matcher Bash, check inside the script (§2 item 7), exit 2; consider "onFailure": "block" (2.1.295) | "never use 3200" |
open-handovers.sh: print docs/handovers/*.md names plus first heading | SessionStart (stdout becomes context; no if, it would never run) | "Start a session by reading the open ones" |
optional: InstructionsLoaded logger to .claude/logs/ | InstructionsLoaded | to confirm the rules from §3.3 load when expected |
| optional: Stop hook proposing CLAUDE.md or handover updates (S5, S10) | Stop | the "end a session with a handover" reminder |
Multi-segment if patterns (Edit(src/routes/...)) are anchored at cwd. For "any depth" use Edit(**/_docs/**). One rule per handler, so two areas need two handlers.
3.6 Skills
| Skill | Frontmatter | From |
|---|---|---|
update-im-web (exists) | add disable-model-invocation: true: it is user-only by its own description, and this keeps its description out of every session | — |
new-experiment | description: Start a new prototype or feature…; runs bun scripts/new-prototype.ts, registry entry, -docs/decisions.md and status.md, nested CLAUDE.md, Components docs group | Two kinds of experiment, new-prototype, registry |
handover | disable-model-invocation: true (invoked as /handover) | Handovers rule: write docs/handovers/YYYY-MM-DD-topic.md, prune finished ones, move lasting items into AGENTS.md or -docs/ |
component-docs-page | paths: "src/routes/**/docs/components/**, src/routes/**/_docs/**" | Components documentation procedure (if it is longer than a rule) |
refresh-media-fixtures | disable-model-invocation: true | "Refreshing media fixtures" section |
stable-copy, perf-run | disable-model-invocation: true, body points to docs/stable-copy.md and docs/performance-testing.md | Commands with ngrok and perf ports |
3.7 Lint, types, tests (see the separate lint audit)
Leave these out of the prompt files once enforced: toLocaleString ban and Intl through format.ts, .inputValidator ban, trailing-slash Links, JSDoc on exports, Tailwind class order (oxfmt), dark: inside DS-token docs, ChartDefinition meta.label and sender (a bun test over all machines), tokens.ts against tokens.css (existing test), fixture byte-identity (test with a hash), devtools piping off (a test that reads vite.config.ts). Plus a route-layout test (§4).
3.8 Proposed minimal AGENTS.md outline (about 60 lines)
- What this repo is (3 lines) and "experiments may depart from im-web on purpose".
- Commands (the existing block), ports: 3100 dev, never 3200, 3300 perf.
- Prototype vs feature (table, 4 rows), registry path.
- Kinds of docs and what binds (the existing table: it is cross-cutting and decides every task).
- Core rules (6 bullets): one folder per experiment; internally consistent and recorded in
decisions.md; the experiment's decisions win, then the working model, then im-web for context only; repo is self-contained (cite Jira, Figma, Bitbucket at a hash); format with Intl; lint and format run automatically by hook. - Session flow: open handovers are listed at start (hook); end with
/handover. - Pointers: "Area-specific rules live in
.claude/rules/(Claude) and the experiment's-docs/", stack line, Related repos (2 lines). <!-- maintainer notes -->for anything humans need but Claude doesn't (stripped, S1).
4. Route file-name patterns for reliable globs
TanStack conventions (S11): _name = pathless layout route (no URL segment, has a layout component); (name) = route group folder (no URL segment, no layout); -name = excluded from the route tree; route.tsx = the route file for a directory; . = flat nesting.
What a glob needs: every file of one kind sits under one literal folder name that no other kind uses, with no characters that a glob engine, gitignore, a regex or the shell treat as special, and with the layout file inside that folder.
| Pattern | URL | Glob (rules / hook if) | Assessment |
|---|---|---|---|
_app/ pathless (+ layout _app/route.tsx, or sibling _app.tsx) | unchanged (/prototypes/x/search) | src/routes/*/*/_app/** / Edit(**/_app/**) | Recommended. _ is literal in every glob engine, in gitignore, in regex and in the shell. A layout is free (app chrome vs DocsShell). URL collisions between docs and app routes are reported by the router generator as conflicting paths (inferred). |
(app)/ route group | unchanged | src/routes/*/*/(app)/** | Parens are literal in gitignore (if, permissions; S6 says no escaping needed). But the rule paths engine is unspecified (§2 item 2), the hook matcher and grep read them as regex groups, and shell scripts need quoting. No layout, so baseline's _public/_checkout nest one level deeper anyway. Avoid. |
app/ URL segment | /prototypes/x/app/search | src/routes/*/*/app/** | Globs are as reliable as _app, but it changes URLs shared with test users, and app could collide with a real segment name. Fine if the URL change is acceptable. |
.-flat names (_app.search.tsx) | — | **/_app.* plus **/_app/** | Mixing flat and directory styles doubles every glob. Avoid. |
Recommendations:
- Per experiment:
<id>/route.tsx(thin),<id>/_docs/**(docs, the default:_docs/index.tsx=/prototypes/<id>),<id>/_app/**(the running prototype, with_public,_checkout,admin,metainside it),<id>/-lib/**,<id>/-docs/**. Put layouts inside their folder (_docs/route.tsx,_app/route.tsx) if the generator accepts it (§2 item 6). Otherwise every glob needs a second entry for_docs.tsx/_app.tsx(brace form in rules:src/routes/*/*/_docs{.tsx,/**}; hookiftakes one rule, so use two handlers). - Use the same name everywhere: today prototypes use
docs/, three features use_docs/, design-system uses the root. Unify to_docsso thatsrc/routes/*/*/_docs/**is the only docs glob. AdocsURL segment inside_docsis fine. - Use
*/*rather than**in front of_appand_docswhen you mean "top level of an experiment":src/routes/*/*/_docs/**matches exactlysrc/routes/{prototypes,features}/<id>/_docs/.**/_docs/**would also catch any nested_docsfolder. The site-level Docs (src/routes/docs/) is deliberately nameddocs, not_docs, so the two stay separable by glob. Keep it that way. - Enforce the layout with a
bun testthat walkssrc/routes/{prototypes,features}/*/and fails on any route file outsideroute.tsx,_app/,_docs/,-lib/,-docs/, so the globs stay true. Havenew-prototype.tsproduce this shape. - Keep
-libshared between docs and app (component previews need the real components). Rules about code go on-lib/**and_app/**, rules about docs on_docs/**and-docs/**.
5. Suggested order of work
- Add permissions (§3.4) and the tokens and port hooks (§3.5). Add
disable-model-invocationtoupdate-im-web. - Create
.claude/rules/from §3.3 and shrink AGENTS.md to the outline in §3.8. Run/doctor prompt-auditand/contextto check. - Rename routes to
_docs/_app(§4), add the route-layout test, updatenew-prototype.tsand the globs. - Add nested per-experiment CLAUDE.md files after checking the router ignores them.
- Re-run
/doctor prompt-auditafter model releases. Review every 3 to 6 months (S10).