1 of 14

Designing an Onion:

Domain-Scoped AI Context at Monorepo Scale

Na'aman Hirschfeld

Principal Systems Engineer @ ServiceNow (Armis)

2 of 14

WHERE WE STARTED

  • One Bazel monorepo, many languages, several products
  • 1,000+ service folders side by side; one product spans 80+ of them
  • No folder you can point at and say “this is Product A”

services/

libs/

frontends/

deploy/

data/

tools/

Product A's code

every other product's code

The question

Product B dev: “show me this customer's details”

Root skill fires

written when the repo held only Product A

Wrong system

queries Product A's system and data

Plausible answer

wrong product, no error, unseen in review

2

3 of 14

PRIOR ART

First try: a plugin marketplace per domain

26

marketplaces

130

plugins

1

enabled by default

  • Far from the code: knowledge sat in a tooling folder
  • Disabled means invisible: agents only see enabled plugins
  • So every engineer had a different subset, and a different repo

3

4 of 14

THE GOAL

Maintain AI context as software

Versioned

Owned

Linted

Reviewed

Tested

Measured

Each session gets its domain's knowledge, and nothing else

4

5 of 14

THE DESIGN

The onion: each layer loads when work reaches it

Load behaviour probed with a unique sentinel token per file, Claude Code 2.1.306.

Repo root

every session start

Standards, a products map, shared skills, generic subagents, the skill index

Area folder

on first Read inside

Its CLAUDE.md, rules and skills, all at once

Service folder

on first Read inside

The same, one level deeper; the closest CLAUDE.md wins, the root still loads

The file

glob-matched rules

Only the rules whose glob matches this file

Grep and Glob load nothing

5

6 of 14

WHERE THINGS LIVE

The repository tree, before and after

Before

repo/

├─ CLAUDE.md

product terms

├─ tools/

│ └─ marketplaces/

26 of them

│ ├─ team-a/plugins/

skills, agents

│ ├─ team-b/plugins/

commands

│ └─ …

├─ services/

│ ├─ a-api/

no context here

│ └─ b-ingest/

or here

└─ frontends/

~/.claude/settings.json

per engineer

After

repo/

├─ CLAUDE.md

standards + map

├─ .claude-plugin/

8 plugins

│ └─ marketplace.json

one marketplace

├─ .claude/

│ ├─ settings.json

declares it

│ ├─ rules/

glob-scoped

│ └─ skills/ agents/

shared, generic

└─ services/

├─ CLAUDE.md

area guide

└─ a-api/

├─ CLAUDE.md

service guide

└─ .claude/

rules, skills

6

7 of 14

THE LAYERS

Nested layers load on first read

Load behaviour probed with a unique sentinel token per file, Claude Code 2.1.306.

  • First Read or Write in a folder loads its CLAUDE.md, rules and skills
  • Grep and Glob load nothing nested, so searching stays clean
  • 6 generic subagents at the root; nested ones are never found
  • A subagent reading a product file gets that folder's layer too

Session start

root layer only

Grep / Glob

nothing nested loads

First Read in a/

the folder is touched

Layer loads

its CLAUDE.md, rules, skills

Subagent reads it

gets the same layer

Probe, don't trust the docs: a unique sentinel token in every file, one question per session

7

8 of 14

THE LAYERS

Rules, skills and what stays at the root

Rules

Always-on, or glob-scoped with paths in the frontmatter. Loaded automatically when a matching file is read.

Skills

Loaded on demand: the model matches the description, or you type /name. Only name and description sit in context.

Gated skills

paths: hidden and uncallable until a matching file is read. disable-model-invocation: slash command only.

Slash commands

A nested skill is unknown until its folder is read, so commands needed from the root live in the root skills.

Root: global skills, plus the skills that must be root slash commands. Everything product-specific: nested, next to its code

8

9 of 14

DISCOVERY

Dynamic skill discovery

ai-rulez: open source, by the speaker — https://github.com/Goldziher/ai-rulez

The problem

  • Claude loads a folder, and with it every skill in that namespace
  • Hunting for a skill by reading folders loads several folders: redundant context

The fix: an index over MCP

  • ai-rulez indexes the repo's skills; find_skill returns ranked, scored matches
  • load_skill returns the one skill needed, nothing else
  • A file watcher can keep the index current (off in our config)

Task

“details for this customer?”

find_skill

ranked, scored matches under the product's path

Choose

the best match for the task

load_skill

only that skill enters the context

9

10 of 14

EVALS I

Two layers of tests: static and dynamic

Static analysis using ai-rulez

free · offline · in CI

  • Calculates the loading and layering, checking what prompt is composed at each stage.
  • What tokens a session sees at start? What terms enter context?
  • Asks: what will it see?

Dynamic evaluation: AI as a judge

local · tens of dollars a run

  • 230 YAML cases, each with criteria
  • Real hermetic claude -p sessions that must answer using skills, measuring recall ("did it use what it needed?") and precision ("was everything it used appropriate?")
  • Asks: what does it do with it?

​

Tune on one split of the cases; a held-out split gives the verdict

10

11 of 14

EVALS II

The static layer is ai-rulez

ai-rulez 5.10.0, pinned in CI and in the harness; a test fails if the pins diverge.

ai-rulez exposure

what a session sees at start, in tokens per phase

exposure leakage

product terms at start; a CI gate that exits 2

exposure collisions

skills in different areas with overlapping listings

ai-rulez validate

the same rules as findings, in CI

areas.toml

the area map: 19 areas with paths, terms and ambiguous words

validate findings

A product term at session start (error)

An ambiguous word with no product term beside it

An acceptance that accepts nothing

7 → 0

product terms at start, kept at 0 by CI

8 accepted, each with a written reason

11

12 of 14

EVALS III

Why trust the numbers

Example case

prompt: show me this customer's details

asker: Product B developer

must_use: b-customer-lookup

must_not: any Product A skill

behaviour: answer, or ask which product

criteria:

- customer means a Product B account

- never uses Product A's records

Regex checks

skill loaded · no forbidden term · command ran

Judge grades

is the answer right, per this case's criteria?

κ 0.669

agreement beyond chance, answers

κ 0.847

agreement beyond chance, criteria

86%

broken answers caught

93%

good answers passed

Decision rule and a 6-read holdout budget, fixed in advance · Sonnet sessions, Opus judge

12

13 of 14

RESULTS

What we tried: a staircase of results

Held-out: 53 cases, two runs each; user-specific context covers 39 of them. Wrong-product answers rest on 5 cases.

1

Wording tweaks

read the guide first, hoist shared skills

no gain

2

Start-hook skill list

a cross-product menu, +16k start tokens

dropped

3

Scoped discovery

+31 recall (tune split); small on held-out

kept

4

User-specific context

developer’s product, confirmed once, in a local file

opt-in

master

layered design

+ user context

Pass rate

39%

81%

96%

Right skill found

22%

72%

94%

Wrong-product answers

70%

0%

0%

/init-command saves a developer’s product, in a git-ignored CLAUDE.local.md loaded at every session start

13

14 of 14

Thank you

Na'aman Hirschfeld

Principal Systems Engineer @ ServiceNow (Armis)