Two codebases, no shared code, no shared domain, no shared tech stack. One is
a Next.js 14 Pages Router app serving millions of listings, Zustand state,
SCSS modules, Optimizely A/B tests, i18n managed via Phrase. The other is a
multi-tenant institutional ERP with 294 view components, 239 database tables,
a Hono backend with 74 route files, four languages including Bengali and
Gujarati, and a 14-role RBAC model where every query must carry a tenant id
or it doesn't ship.
I built a Claude Code layer for both. Same core idea: a set of skills, commands, hooks, and memory files that turn Claude Code from a capable assistant into something closer to a second engineer who already knows the codebase's rules. By the time I was working across both projects in parallel, the marketplace product had 35 commands, seven routed skills, and a registered subagent contract; the ERP had 47 skills; both had 14 hooks. This is what I learned sorting out which pieces travelled and which ones had to stay home.
What a skill actually is here
"Skill" gets used loosely, so here is precisely what the seven marketplace
skills are. Each one is a SKILL.md with YAML frontmatter and a procedure
body, and every one of them is a scanner and a fixer for one domain. The
frontmatter carries three things that do real work: a description written
as trigger phrases ("Use when: fix React hooks, scan async patterns, review
performance…") so the model can route natural-language requests to the right
skill; an applies_to glob list that declares which files the skill cares
about, which is what the deterministic routing intersects against; and an
argument hint so scan versus fix plus a scope can be passed like a CLI
flag. The body is the same shape in every skill: determine scope (changed
files against main by default), choose mode (scan is read-only findings
with severity ratings; fix applies changes and then validates with
yarn lint, tsc --noEmit, and Vitest on the affected files), then run the
domain's checks.
The depth lives one level down, in each skill's references/ directory —
two dozen reference documents across the seven skills, under an explicit
lazy-load discipline: the default is do not read them. code-quality
carries seven (naming conventions, dead code, import hygiene, file size and
splitting for anything over 200 lines, config and magic numbers, duplication,
GraphQL codegen) and loads each only when the scope warrants it — the
duplication checklist on multi-file scopes, the GraphQL one only when
generated types are touched. react-and-async carries five: React patterns
(hook dependencies, stale closures, memory leaks), async patterns (abort
controllers, race conditions, Promise handling), Next.js and server patterns,
and performance. a11y-i18n carries the accessibility checklist
(label-input pairing, ARIA, semantic HTML, keyboard nav), the i18n checklist
(hardcoded strings, locale-file sync, unused keys), and the CSS one (design
tokens, !important, dead classes). security-and-testing splits by file
type: the security checklist (XSS, input validation, credential exposure)
for source files, separate Vitest, Playwright, and Storybook checklists that
only load when tests, E2E specs, or stories are in scope, and an
error-handling checklist that loads when a try/catch or error boundary
appears.
The two remaining skills are the most codebase-opinionated. feature-flags
knows the shape of toggles.ts: it inventories every toggle with its
clientSideScope, ticket reference, and expiration date, flags toggles used
on pages outside their declared scope, finds toggles hardcoded to true with
a TODO, and in fix mode retires a concluded experiment properly — remove the
check, keep the winning code path, delete the definition, clean the dead
code. pr-workflow owns hygiene: PR scope analysis, a pre-commit quality
gate, and comment resolution. And every skill honours one shared contract
file that spells out the orchestrator handshake: accept hunks or whole files
as scope, never expand beyond it, emit findings in the strict one-line
schema, and never cite a file you haven't read this turn.
That last constraint is the design's actual thesis. A skill here isn't a prompt with a name. It's a contract: declared inputs, two modes, bounded scope, machine-checkable output, and opinions specific enough to this codebase to be worth enforcing.
What travelled without modification
The session-boundary hooks moved verbatim. A SessionStart hook that runs
git diff --name-only main and prints the changed files costs thirty lines of
shell. I wrote it once for the marketplace product and dropped it into the ERP the
same afternoon. Same with the Stop hook that runs tsc --noEmit, if the
session ends and the build is broken, I want to know before I close the
terminal. Neither hook knows what the project is about. They just enforce a
discipline that applies everywhere.
The claim-verifier hooks travelled just as cleanly, and they earn their
keep more than anything else in the layer. A Stop hook re-reads the
assistant's final message and checks two things. First, citations: if the
message references a file it never read this turn ("called from app.ts",
"fixed at line 140"), that's flagged as a hallucination — the read set comes
from the run's audit log. Second, negative claims: statements like "no other
usages" or "0 occurrences" are re-executed as real rg / git grep checks,
and mismatches surface as [verifier-mismatch]. A companion PreToolUse hook
simply denies edits to __generated__/ directories — regenerate, don't
hand-patch. None of this knows anything about marketplaces or ERPs. It's
epistemic hygiene, and it ports anywhere.
The memory structure pattern transferred completely. Both repos now have a
memories/repo/ directory with five files: modules.md (what each part of
the codebase owns), decisions.md (architecture calls and why they were made),
conventions.md (patterns discovered by reading, not documented anywhere),
known-flakes.md (a registry of test failures that are noise, not signal),
and lessons.md, an append-only file written by a /lesson command whenever
something cost me an hour it shouldn't have. Claude reads these at the start
of relevant sessions. The structure is identical; the contents are obviously
not.
The linting post-hooks (Prettier, ESLint, Stylelint) moved with zero changes. Formatting a file after an edit is mechanical and project-agnostic. The point is that the lowest-level hooks, the ones closest to the file system are the most portable by a wide margin.
What needed a rewrite at the seam
In the marketplace product, /implement and /review-pr are orchestrators
that own the workflow and deliberately own none of the rules. The rules live
in seven domain skills — code-quality, react-and-async, a11y-i18n,
security-and-testing, feature-flags, pr-workflow, and the implement
orchestrator itself. Each skill declares an applies_to glob list in its
frontmatter and keeps its depth in a references/ directory that's only read
when the skill actually runs, so a session never pays for rules it doesn't
need. Routing is not left to the model's judgment: a thirty-line shell script
intersects the changed-file set with every skill's applies_to globs and
prints exactly which skills should run, in a canonical order, and a
UserPromptSubmit hook runs it automatically and injects the result into the
prompt as [skills-routed: …]. Docs-only diff? No skills run at all. Findings
come back in a strict one-line schema — file:line rule-id message — because
the orchestrator post-processes them, and prose doesn't post-process.
The ERP couldn't reuse any of that routing, because its risk isn't
file-type-shaped, it's domain-shaped. Trying to maintain one CLAUDE.md for
239 tables and 58 architectural rules wasn't viable after month two. I
switched to 58 separate rule files in .claude/rules/, each covering one
concern (sql-safety.md, fee-ledger.md, mutation-wiring.md,
state-machines.md, and so on), with @-imports pulling in only the rules
relevant to the current task. The orchestration concept survived; the file
structure that supports it had to be rebuilt from scratch.
The same split happened with hooks. In the marketplace product, the
post-tool-use hooks are about code style: ESLint, Prettier, Stylelint, then
tsc. In the ERP, those run first, and then eight domain-validator hooks run
after every file write: check-ddl-safety.sh (no unsafe migration patterns),
validate-service-pattern.sh (every service file must implement both the local
PGlite adapter and the remote Hono adapter), check-seed-version-sync.sh
(SEED_VERSION must stay in sync after schema changes), detect-dead-buttons.sh
(no UI button left without a wired handler), and four more. You don't write
those for a marketplace product. The ERP's failure modes are different, a
misconfigured service adapter silently falls back to local data in production;
a dead button in a fee-payment form is a support ticket.
What I deleted and rebuilt as something better
The marketplace product has three A/B test commands: ab-test-kickoff.md,
ab-test-health.md, ab-test-wrapup.md. They know about the experiment
framework's IDs, the internal flag-naming convention, and the cleanup pattern
when an experiment concludes. They're genuinely useful on that codebase. They're
completely meaningless anywhere else.
I made the mistake of trying to generalise them. I spent an afternoon writing a "generic experiment workflow" skill that accepted the A/B framework as a parameter. Nobody used it because it had no opinions, and skills without opinions aren't useful, they just become a more verbose way to write a prompt. The right lesson was: keep the experiment skills in the marketplace product, accept that the ERP's equivalent problem (feature rollouts behind RBAC gates) needs its own dedicated tooling, and stop trying to make one thing do both jobs.
The marketplace product's scan-stores.md command is a similar story. It audits
Zustand store definitions for common problems: stale selectors, missing
shallow equality checks, state that belongs in a URL instead. It's a good
command. The ERP uses TanStack Query for server state and Zustand only for
ephemeral UI state. A wholesale port would have been useless. I wrote a much
narrower refactor-state.md skill that handles the patterns that actually come
up there, then moved on.
The one structural decision that changed everything
Both projects ended up leaning on subagents, and it took me a while to notice they were solving opposite problems with the same mechanism.
In the marketplace product, the split is scan versus fix. Scan-mode skill
runs are dispatched to a read-only Explore subagent: isolated context,
constrained tool envelope, structured findings back, nothing else. Four scan
skills run concurrently that way, because an isolated context that returns
twelve schema-formatted findings is strictly cheaper than four skills flooding
the orchestrator's conversation. Fix mode always runs in the main agent,
because subagents can't apply edits. The part that made this reliable wasn't
the dispatch — it was the registry. Every subagent a command may reference is
declared in one AGENTS.md file with its purpose, tool allowlist, and default
thoroughness, and a validator script fails the config if a command references
an agent that isn't registered. Without that, a command saying "use the
Critic subagent" is just prose that fails silently at dispatch time. Every
dispatch is logged by a SubagentStop hook and rolled up into a
/usage-stats command, so I can see which delegations actually pay.
The ERP's subagents compose the other way: they're workers inside larger
orchestrations. port-to-backend.md creates a Hono route, wires up the
middleware stack, updates the service file with a dual-mode adapter, and
returns a summary, and a migration session might run five of those
back-to-back. In a project with 74 backend route files and a strict adapter
pattern that every one of them must follow, the ability to delegate a
well-defined sub-task to an agent with a limited tool allowlist is the
difference between orchestrating a migration in two hours and babysitting it
for two days.
Same mechanism, two jobs: isolation for cheap parallel review, delegation for bulk mechanical work. The registry-plus-validator discipline is what makes either safe.
The tooling is earning its keep. The session-boundary hooks, the claim verifier, the memory files, the domain-validator hooks in the ERP, these have all paid back the time it took to write them. The things that failed were either too generic to be useful, or too specific to one codebase to survive the move. The skill that travels is the one with strong opinions that happen to be correct for the problem in front of it. That's not a rule about AI tooling. That's just a rule.