CLAUDE.md · git:20260825.e3fa96c · 2026-08-25 · sha256 f1dfb04d078c3cd2
CLAUDE.md git:20260825.e3fa96cA
Immutable. This exact content never changes and is served at /api/v1/blob/f1dfb04d078c3cd2.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
A package manager for AI coding-agent skills — npm/Homebrew, but the install target is N
heterogeneous coding agents (Claude Code, Codex) rather than one runtime. That single fact
drives the whole architecture.
Deep background lives in [ARCHITECTURE.md](ARCHITECTURE.md) (how the code is shaped and why)
and [DESIGN.md](DESIGN.md) (the artefacts: manifest, registry protocol, adapter contract, CLI
UX). Read those before any structural change.
## Commands
```bash
npm run verify # everything CI runs — do this before saying you are done
npm run build # tsc --build across all 7 packages (project references)
npm run test:only # tests without rebuilding
npm test # build + tests
npm run lint # eslint
npm run format # prettier --write
npm run check:boundaries # architecture dependency rule (see below)
npm run registry:build # regenerate registry/skills.yaml
```
Run the CLI from source:
```bash
npm run agent-skills -- install java-performance --dry-run
node packages/cli/bin/agent-skills.mjs --help
```
Single test file, or a single test:
```bash
node --test packages/core/test/resolver.test.ts
node --test --test-name-pattern="detects a cycle" packages/core/test/resolver.test.ts
```
Tests use `node:test` with **no test framework** and run the TypeScript sources directly via
Node's native type stripping. `test:only` skips the build; cross-package imports
(`@jvm-expert/core`) still resolve to `dist`, so **build first if you changed another
package**.
## Two TypeScript constraints that will bite you immediately
`erasableSyntaxOnly` is on, because Node executes these sources directly in tests:
- **No `enum`** — use `const X = {...} as const` plus a union type. See `domain/errors.ts`.
- **No parameter properties** — `constructor(private readonly fs: FileSystem)` does not
compile. Declare the field, then assign in the constructor body.
- No `namespace`, no non-declare class field initialisation tricks.
**Relative imports carry the `.ts` extension** (`./errors.ts`, not `./errors.js`).
`rewriteRelativeImportExtensions` emits `.js` at build time. Getting this wrong breaks either
the build or the test run, depending on which way you get it wrong.
## Architecture: the parts that need multiple files to understand
Seven packages, one dependency rule:
```
cli ──────► core ◄────── registry
│ ▲ ▲
│ installer adapter-claude
│ adapter-codex
└──────► node ─────────────┘
```
| Package | Owns | May import |
| ---------------------------------- | ---------------------------------------------------------------- | --------------------- |
| `core` | Domain model, application services, **all port interfaces** | nothing that does I/O |
| `node` | The only place `node:fs`, `child_process`, `fetch`, `tar` appear | core + built-ins |
| `registry` | Local/Git/HTTP drivers + precedence federation | core |
| `installer` | Atomic install, path safety, archive extraction | core |
| `adapter-claude` / `adapter-codex` | One agent's layout + detection | core (+ `node:path`) |
| `cli` | Commander, rendering, **composition root** | everything |
`npm run check:boundaries` enforces this and runs in CI. `core` may not import `node:fs` **or
`node:path`** — the domain carries its own POSIX path helper (`domain/posix-path.ts`) so it
behaves identically on every OS. If you add a package, register it in
`scripts/check-boundaries.mjs` or it is silently unchecked.
### Three invariants that are easy to violate
**1. Only `AtomicInstaller` writes into an agent's skill directory.**
`AgentAdapter` deliberately has **no `install()` / `uninstall()`**. Adapters _describe_ the
target (`locationFor`) and _project_ the package (`layoutFor`, a pure function returning a plan).
Every write goes through one hardened commit path, so atomicity, rollback, ownership tracking
and path-traversal defence exist once rather than once per agent.
**2. No agent-specific logic outside `packages/adapter-*`.**
If you are writing `if (agent === 'codex')` anywhere else, the design is telling you something.
The escape hatch for per-agent _presentation_ metadata is `agentOverrides` in the manifest,
whose keys each adapter allowlists via `overrideKeys`.
**3. Path safety has exactly one implementation:** `core/domain/path-safety.ts`.
Both `validate` (is this package well-formed?) and the extractor (is this archive hostile?)
wrap it. These were once separate and had already drifted — a package `validate` accepted could
be one the extractor rejected. Do not add a second copy.
### Where things live
- **Ports** — `core/src/ports/`. Small on purpose. Adding a method is a real design decision.
- **Application services** — `core/src/application/`. Own ordering and policy; perform no I/O
themselves.
- **Composition root** — `packages/cli/src/container.ts`. The only file that constructs
concrete classes. Registering a new adapter is one `.register(...)` call here.
- **Test doubles** — `@jvm-expert/core/testing` (`InMemoryFileSystem`, `FakeRegistry`,
`buildPackage`, …). Exported from the package, not hidden in a test folder, so out-of-tree
adapters can use them too. `core`'s own tests cannot import `@jvm-expert/registry` (that
would invert the dependency rule), hence `core/test/helpers/federation-double.ts`.
## Never let a test or a manual run touch the real agent config
Anything that installs must be pointed at a temp directory first:
```bash
export AGENT_SKILLS_HOME="$SANDBOX/.agent-skills"
export CLAUDE_CONFIG_DIR="$SANDBOX/.claude"
export CODEX_HOME="$SANDBOX/.codex"
export HOME="$SANDBOX" USERPROFILE="$SANDBOX" # project discovery reads the home dir
```
Set `HOME`/`USERPROFILE` too, not just the three overrides: project-root discovery walks up
from the cwd and needs to know where "home" is. Without it, a run from a temp directory under
the real `$HOME` can resolve project scope to the user's home and write into their actual agent
configuration. This has happened.
`packages/cli/test/cli.test.ts` does this correctly — copy its `cli()` helper rather than
reinventing it. It also clears `PATH` so a real `claude`/`codex` binary cannot influence
detection.
## Agent conventions are verified facts, not guesses
| Agent | Kind | Global | Project |
| ----------- | -------- | ----------------------------------------------------- | ----------------------------- |
| Claude Code | skill | `$CLAUDE_CONFIG_DIR/skills` → `~/.claude/skills` | `<project>/.claude/skills` |
| Claude Code | command | `$CLAUDE_CONFIG_DIR/commands/<name>.md` | `<project>/.claude/commands` |
| Claude Code | workflow | `$CLAUDE_CONFIG_DIR/workflows/<name>.js` | `<project>/.claude/workflows` |
| Codex | skill | `$CODEX_HOME/skills` → **`~/.codex/skills`** | `<project>/.agents/skills` |
| Codex | command | none — `locationFor` returns `undefined` (unverified) | none |
| Codex | workflow | none — `locationFor` returns `undefined` (unverified) | none |
A package declares which it is with `kind` in `skill.yaml` — `skill` (default), `command` or
`workflow` — and the entrypoint follows: `SKILL.md`, `COMMAND.md` or `WORKFLOW.js`. A skill
installs as a directory; a command and a workflow each install as a single file named after the
package. `AgentLocation.shape` carries that, and every shape commits through the same rename in
`AtomicInstaller`.
A workflow is JavaScript, so its identity comes from `export const meta = { … }`, read by the
literal parser in `domain/js-literal.ts` — **never executed**. `parseEntrypoint` normalises both
forms into one `SkillDocument`, which is why validation, search and `info` never branch on kind.
The determinism rules (no `Date.now()`/`Math.random()`/`new Date()`) are Claude Code's own and
live in the Claude adapter.
Codex's global path was extracted from the `codex` binary itself. Widely repeated
documentation says `~/.agents/skills`; that is the _repository_ convention, not the user-global
one. **Do not "fix" this.** The skill roots are config-overridable per agent
(`agents.<id>.globalRoot` / `projectRoot`); those keys name the skills root only.
When adding an agent, verify against the real binary — see
[docs/adding-an-agent.md](docs/adding-an-agent.md).
## Generated files and their trap
`registry/skills.yaml` is generated by `scripts/build-registry-index.mjs` and is in
`.prettierignore` (its formatting comes from our own YAML serialiser, and prettier would fight
`registry:check` on every commit).
Package integrity is a hash over file **contents**, so **any edit under `skills/` changes the
integrity — including a prettier reformat of a `skill.yaml`**. After touching `skills/`:
```bash
npm run registry:build
```
`npm run verify` fails if you forget.
## Public contracts — changing these is a breaking change
- **`ASK_*` error codes** (`core/src/domain/errors.ts`) appear in `--json` output and in
[docs/errors.md](docs/errors.md). They never change meaning; new ones may be added.
- **Exit codes** distinguish usage (2) / validation (3) / resolution (4) / security (5) /
no-agent (6). Do not collapse them to 1.
- **`schemaVersion`** on manifests, indexes and lockfiles. Additive fields ship as _optional
under the current number_; bump only for a genuinely breaking change.
## Conventions
- **Errors**: throw `AgentSkillsError` with a code, `details`, and `hints` (concrete next
commands). Infrastructure errors are translated at the port boundary — no layer above should
ever interpret an `ENOENT`.
- **Validation** returns every problem it can find (`IssueCollector`) rather than throwing on
the first; only "this cannot be loaded at all" throws.
- **Commits**: Conventional Commits (`feat(installer): …`). See
[CONTRIBUTING.md](CONTRIBUTING.md).
- Security-relevant changes want an adversarial test (hostile fixture), not a happy-path one.
`packages/installer/test/security.test.ts` is the pattern.
## Note
An OpenAI Codex config exists at `~/.codex/config.toml`. If you want its user-level items (MCP
servers, slash commands, subagents, skills, instructions) available in Claude Code, reply
`/import` to scan and list what is importable, then `/import --yes=<digest>` to apply it. If
`/import` is unavailable on this surface, run `claude import` from a terminal instead.