This is the developer reference for the agentry repo — a per-file, per-module, per-flow map of what the codebase does. Read this when you need to know exactly what a given script does, where a given file lives, or what the sync engine touches in what order.
For the why behind the shape, see decisions.md. For high-level design, see architecture.md. For how to write new content, see authoring.md. This document does not restate any of that — it cross-references.
| Path | Kind | What it is |
|---|---|---|
agents/<name>.md |
source | Authored agents (Claude-style frontmatter + body). Edit here. |
skills/<name>/SKILL.md |
source | Authored skills (frontmatter + body, may bundle siblings). Edit here. |
commands/<name>.md |
source | Authored slash commands. Edit here. |
rules/<category>/<name>.md |
source | Authored rules, namespaced by category (language identifier or topic). Edit here. |
hooks/<name>.js |
source | Authored harness hooks (Node scripts, seven of them). Edit here. See Hook reference. |
mcp/<name>.json |
source | Authored MCP server definitions, one per file; filename is the server name. Edit here. |
scripts/sync-harnesses.js |
tool | Sync engine. Generates .claude/, .cursor/, .codex/, .opencode/, and the MCP config files from sources. |
scripts/frontmatter.js |
tool | Shared YAML-ish frontmatter parser and validation helpers. |
scripts/cursor-transform.js |
tool | toCursorRule, globsForLanguage — Cursor .mdc rule transform and language-glob mapping. |
scripts/codex-transform.js |
tool | renameSkill, agentToSkill, ruleToSkill — Codex adapter transforms. |
scripts/opencode-transform.js |
tool | agentToOpenCodeAgent, commandToOpenCode — OpenCode adapter transforms. |
scripts/mcp-transform.js |
tool | validateServer, toMcpServersJson, toOpenCodeMcpConfig — MCP adapter transforms. |
scripts/lint-frontmatter.js |
tool | npm run lint — frontmatter (agents/skills) and MCP server validation. |
scripts/doctor.js |
tool | npm run doctor — installation health check. |
scripts/install.sh |
installer | POSIX installer (Unix/macOS). |
scripts/install.ps1 |
installer | PowerShell installer (Windows). |
tests/*.test.js |
test | Unit tests run by npm test using Node's node:test runner. |
docs/architecture.md |
doc | High-level design and adapter pattern. |
docs/authoring.md |
doc | How to author new agents, skills, rules. |
docs/decisions.md |
doc | Numbered design decisions with rationale. |
docs/reference.md |
doc | This document. |
.claude/{agents,skills,commands,rules,hooks}/ |
generated | Claude Code adapter output. Do not edit. Wiped on sync. |
.cursor/{agents,rules}/ |
generated | Cursor adapter output. Do not edit. Wiped on sync. |
.codex/agents/skills/ |
generated | Codex adapter output. Do not edit. Wiped on sync. |
.opencode/{agents,skills,commands}/ |
generated | OpenCode adapter output. Do not edit. Wiped on sync. |
.claude-plugin/plugin.json |
generated | Claude Code plugin manifest. Written by syncClaude; every field read from package.json. Do not edit. |
.mcp.json |
written | Merged MCP server map for Claude Code (project scope, repo root). Written by syncClaude when mcp/ sources exist, never deleted — may carry your own servers (D20). |
.cursor/mcp.json |
written | Merged MCP server map for Cursor. Written by syncCursor when mcp/ sources exist, never deleted — may carry your own servers (D20). |
opencode.json |
written | MCP config for OpenCode (repo root, mcp key, translated shape). Written by syncOpenCode when mcp/ sources exist, never deleted — may carry your own OpenCode config (D20). |
.claude/settings.local.json, .cursor/environment.json, .codex/config.toml |
user state | Per-user harness state at the top of a harness directory. Sync never touches it: an adapter wipes only the subdirectories it generates, never the parent. |
.gitattributes |
config | Forces LF line endings (* text=auto eol=lf) so sync output is byte-identical across platforms. |
.github/workflows/sync-check.yml |
ci | Three-job CI workflow (sync determinism, lint, tests). Scoped to contents: read. |
.github/workflows/release.yml |
ci | On a v* tag push, gates on the same three checks and cuts a GitHub Release from the matching CHANGELOG section (D21). Needs contents: write. |
.github/dependabot.yml |
ci | Weekly grouped updates for the npm manifest and the GitHub Actions used by the workflows. |
.github/PULL_REQUEST_TEMPLATE.md |
doc | Surfaces CONTRIBUTING's three hard PR requirements at the moment a PR is opened. |
.github/ISSUE_TEMPLATE/ |
doc | bug_report.yml, feature_request.yml (a proposal form), and config.yml, which disables blank issues and routes security reports to a private advisory. |
.gitignore |
config | Tracks generated harness dirs; ignores Claude Code per-user state. |
package.json |
config | npm scripts, Node engine requirement, and the single source of the project metadata syncClaude writes into the plugin manifest. Marked private — agentry installs by clone, never from a registry. |
README.md |
doc | Project overview and install. End-user entry point. |
CONTRIBUTING.md |
doc | Contributor workflow and rejection criteria. |
CLAUDE.md |
doc | AI assistant operating guidance for this repo. |
CHANGELOG.md |
doc | Per-version changes. |
LICENSE |
legal | MIT. |
SECURITY.md |
doc | Threat model for a config generator, and the private advisory flow for reporting a vulnerability. |
CODE_OF_CONDUCT.md |
doc | Contributor Covenant 2.1. |
These content types ship today. Each lives in a separate top-level directory and has its own contract. For the how-to-author flow, see authoring.md; this section documents the contract only.
- Location:
agents/<name>.md. Filename without.mdmust equal thename:field. - Required frontmatter:
name(kebab-case),description(≥ 20 chars),tools(literal string, typically a bracketed list like[Read, Grep, Bash]),model(typicallysonnet). - Optional frontmatter: none recognized today. Extra fields are preserved by the sync engine for Claude Code and Cursor; dropped by Codex.
- Body: Markdown. Becomes the agent's system prompt in Claude Code, copied verbatim to Cursor, converted to a skill body for Codex.
- Location:
skills/<name>/SKILL.md. Directory name must equal thename:field. Siblings ofSKILL.md(e.g.scripts/,references/) are copied through to Claude Code and Codex. - Required frontmatter:
name,description(≥ 20 chars). - Optional frontmatter: none recognized today. Extra fields pass through.
- Body: Procedural markdown. Loaded into context when the skill matches.
- Location:
commands/<name>.md. Filename becomes the slash-command name (/<name>). - Required frontmatter:
description. (Claude Code does not requirenamefor commands.) - Optional frontmatter:
argument-hint(string shown in the command prompt UI). - Body: Markdown instructions that run when the user invokes
/<name>.$ARGUMENTSis substituted at invocation time. - Harness support: Claude Code and OpenCode. Cursor and Codex skip commands — see
decisions.mdD8.
- Location:
rules/<category>/<rule-name>.md. Category is a language identifier (typescript,python,go) or topic (security,performance). - Required frontmatter:
name(matches filename without.md),description(≥ 20 chars). - Optional frontmatter:
language(used since v0.6 to derive Cursor auto-attach globs — seeglobsForLanguageinscripts/cursor-transform.js). - Body: Tight, single-concern guidance. No code samples.
- Harness support: Claude Code (verbatim copy); Cursor (
.mdc, auto-attached vialanguage-derived globs since v0.6, otherwisealwaysApply: false); Codex (converted to a skill viaruleToSkillsince v0.6). OpenCode deferred — its rules model isAGENTS.mdplus theinstructionsconfig.
- Location:
mcp/<name>.json. Filename without.jsonis the server name (the map key in the harness); there is no name field in the file. - Format: JSON, not frontmatter. The file is the server definition object as it appears inside Claude Code / Cursor's
mcpServersmap. - Required: exactly one transport — a non-empty
command(stdio) or a non-emptyurl(remote). - Optional:
args(array) andenv(object) for stdio servers;type, authheaders, and any other harness-recognized fields for remote servers. - Harness support: Claude Code (
.mcp.json) and Cursor (.cursor/mcp.json) take themcpServersmap verbatim; OpenCode (opencode.json,mcpkey) takes a translated shape (type: local|remote,commandarray,environment,enabled). Codex deferred — seedecisions.mdD20. - Secrets: do not inline tokens; reference an environment variable the harness expands (
"env": { "API_KEY": "${TOKEN}" }).
The sync entry point. Parses CLI flags, dispatches to one or more adapters, and writes the harness-specific tree.
- No exports. Side-effect-only script invoked by
npm run sync. - Internal structure:
SOURCES— map of source directory absolute paths (agents, skills, commands, rules, hooks, mcp).ALL_TARGETS—["claude", "cursor", "codex", "opencode"].parseTargets(value)— splits a comma list of targets, separates valid from unknown.- File helpers —
rel,exists,readDirSafe,copyFile,writeFile,rmGenerated,copyTree. All respect--dry-run. - Adapters —
syncClaude,syncCursor,syncCodex,syncOpenCode(documented inline with JSDoc). ADAPTERS— dispatch map from target name to adapter function.
- Imports:
node:fs/promises,node:path,node:url, and the four transform modules (cursor-transform,codex-transform,opencode-transform,mcp-transform). No third-party deps.
Shared frontmatter parser and validation helpers. Call sites: lint-frontmatter.js, doctor.js, and the cursor-transform.js, codex-transform.js, and opencode-transform.js transforms.
parseFrontmatter(content)— Parses the leading---...---block. Returns{ fields, body, raw }ornullif no block is detected. Accepts CRLF. Treats array-shaped values (tools: [Read, Grep]) as literal strings — no structured array parsing.checkRequired(fields, requiredKeys)— Returns the subset ofrequiredKeysabsent or empty infields. Preserves input key order.checkDescription(desc, minLength = 20)— Returns"missing or empty","too short (N chars, minimum M)", ornullwhen valid.
toCursorRule(content, opts = {})— Translates source skill or rule content to a Cursor.mdcrule. Behaviors:- No frontmatter in source → wraps content in a new block with the optional
globsandalwaysApply: false. - Frontmatter without
alwaysApply→ appendsglobs(when provided and absent) andalwaysApply: falseto the existing block. - Frontmatter already declaring
alwaysApplyorglobs→ preserved as-is (no duplication). - Body is separated from the closing
---by exactly one blank line, regardless of source spacing.
- No frontmatter in source → wraps content in a new block with the optional
globsForLanguage(language)— Returns the comma-separated Cursor glob patterns for a language identifier (fromLANGUAGE_GLOBS), ornullif unmapped. Case-insensitive.LANGUAGE_GLOBS— The language → globs map (TypeScript, Python, Go, Rust, and others).- Imports:
parseFrontmatterfromfrontmatter.js.
renameSkill(content, newName)— Rewrites only thename:line. All other fields and body preserved verbatim. Returnsnullif source has no frontmatter.agentToSkill(content, newName)— Strips an agent's frontmatter down toname(set tonewName) anddescription. Dropstools,model, and any other fields. Body preserved verbatim. Returnsnullif source has no frontmatter.ruleToSkill(content, newName)— Converts a rule to a Codex skill:name(set tonewName) anddescription, dropping rule-specific fields likelanguage. Description falls back to the first#heading, then a generic label, so a rule with no frontmatter still produces a described skill. Never returnsnull.- Imports:
parseFrontmatterfromfrontmatter.js.
agentToOpenCodeAgent(content)— Translates an agent to OpenCode's shape: keepsdescription, addsmode: subagent, dropsname(filename-derived),tools, andmodel. Body preserved verbatim. Returnsnullif source has no frontmatter.commandToOpenCode(content)— Keepsdescription, dropsargument-hint. Body — including$ARGUMENTS— preserved verbatim. Returnsnullif source has no frontmatter.- Imports:
parseFrontmatterfromfrontmatter.js.
validateServer(def)— Semantic check for one parsed MCP server definition. Returns an array of error strings (empty if valid): rejects non-objects, requires a non-emptycommandorurl, and rejects a non-arrayargsor non-objectenv. Used bylintanddoctor, not by sync.toMcpServersJson(servers)— Builds{ "mcpServers": { <name>: def } }(Claude Code.mcp.jsonand Cursor.cursor/mcp.json) from an array of{ name, def }. Sorts by name; does not mutate the input. Trailing newline.toOpenCodeMcpConfig(servers)— Builds{ "$schema": ..., "mcp": { <name>: openCodeDef } }(OpenCodeopencode.json), translating each server to OpenCode's shape (type: local|remote,commandarray,environment,enabled). Sorts by name.- Imports: none. Pure functions over plain objects.
The npm run lint script. Iterates every agents/*.md, skills/<name>/SKILL.md, and mcp/<name>.json, validates each, exits 0 if all pass and 1 if any fail.
- No exports.
- Internal functions:
lintAgent(file),lintSkill(skillDir),lintMcpServer(file)(JSON parse +validateServer), plus a localrel/readDirSafepair. - Coverage caveat: lints agents, skills, and MCP servers. Commands and rules are not linted by this script today. The MCP section header prints only when
mcp/has sources.
The npm run doctor script. Reports the health of sources, generated dirs, frontmatter, and the user's Claude Code install.
- No exports.
- Checks performed:
- Source directories — agents and skills exist and are non-empty; hook and MCP-server counts reported (both optional, so absence is informational).
- Generated
.claude/,.cursor/, and.opencode/contain the expected file per source. - Generated MCP outputs —
.mcp.json,.cursor/mcp.json(mcpServerskey), andopencode.json(mcpkey) parse and contain every source server (viacheckMcpOutput). - Frontmatter on every agent and skill — own
validateFieldshelper, notfrontmatter.js'scheckRequired. - MCP server JSON — each
mcp/<name>.jsonparses and passesvalidateServer. - User install at
~/.claude/— lists installed agents and skills, flags missing ones. (Does not check Cursor, Codex, or OpenCode installs.)
- Exit: 0 if every required check passes, 1 on failure.
Seven Node scripts under hooks/, copied verbatim into .claude/hooks/ by syncClaude. They sync to Claude Code only; no other harness has a drop-in hooks directory.
Nothing enables them. Sync copies the files; you wire each one into settings.json yourself as a PreToolUse hook with the appropriate matcher. Read a hook before you turn it on — each carries its own wiring snippet in its header comment.
The contract is a process contract, not a function signature: Claude Code passes the tool call as JSON on stdin; the hook exits 0 to allow and 2 to block, writing the reason to stderr, which Claude sees. Every hook fails open — an empty, malformed, or unexpected payload exits 0. A hook that throws returns non-zero and would block every tool call in the session.
None of them inspects tool_name; the matcher in settings.json is what routes the right calls to them.
| Hook | matcher |
Blocks |
|---|---|---|
protect-generated-dirs |
Write|Edit|MultiEdit |
A write into a generated subdirectory. Names the source file to edit instead. Deliberately allows the per-user state and MCP config files listed in the inventory above. |
secret-scan-on-edit |
Write|Edit |
Content carrying a credential (AWS key id, GitHub PAT, private-key header). Reads an Edit's new_string as well as a Write's content. Allows a process.env reference and a placeholder in .env.example. |
block-secret-file-stage |
Bash |
A git add / git commit -a that would stage a credential file. Resolves pathspecs by asking git (git add --dry-run), so the sweeping git add . is caught, not just an explicitly named path. Respects .gitignore. |
block-no-verify |
Bash |
--no-verify, -n, --no-gpg-sign, -c commit.gpgsign=false on a git invocation. |
block-force-push |
Bash |
A force-push to main/master, including --force-with-lease. A feature branch is yours to rewrite. |
guard-dangerous-bash |
Bash |
rm -rf of /, /*, ~, $HOME; chmod -R 777 /; curl | sh; dd of=/dev/sd*; mkfs; the fork bomb. Intentionally narrow — rm -rf ./build and sudo rm -rf /var are not blocked. |
protect-lockfile-edit |
Write|Edit |
A hand-edit to a dependency lockfile across twelve ecosystems (npm, yarn, pnpm, bun, Cargo, poetry, uv, Pipfile, Gemfile, composer, go.sum, npm-shrinkwrap). |
What happens when you run npm run sync (or node scripts/sync-harnesses.js).
Flags recognized:
--dry-run— log every operation as[dry]without touching the filesystem.--target <name>or--target=<name>— restrict to one target. Comma lists are supported (--target claude,cursor).- Unknown flags set
process.exitCode = 1and are reported to stderr but do not abort. - Unknown targets are collected and reported at the start of
main()but do not abort the run.
Default behavior with no --target flag runs all four adapters in order: claude, cursor, codex, opencode.
syncClaude() — runs in this order:
- Wipe
.claude/agents/,.claude/skills/,.claude/commands/,.claude/rules/,.claude/hooks/. Does not wipe the parent.claude/(Claude Code stores per-user state there, e.g.settings.local.json— seedecisions.mdD5). - Copy each
agents/<name>.mdto.claude/agents/<name>.mdverbatim. - Copy each
skills/<name>/directory tree (including siblings ofSKILL.md) to.claude/skills/<name>/. - Copy each
commands/<name>.mdto.claude/commands/<name>.mdverbatim. - For each
rules/<category>/<name>.md, copy to.claude/rules/<category>/<name>.mdverbatim. Then copy thehooks/tree to.claude/hooks/. - If
mcp/has sources, merge them vialoadMcpServers+toMcpServersJsonand write.mcp.jsonat the repo root (mcpServersmap). Written only when sources exist; never deleted. - Write
.claude-plugin/plugin.jsonwith the inline manifest (name, version, description, author, license).
syncCursor() — runs in this order:
- Wipe
.cursor/agents/and.cursor/rules/. Does not wipe the parent.cursor/— Cursor stores per-user state there, notablyenvironment.json. (This adapter used to remove the whole tree, which deleted that state on every sync and made theagentry-prefix in step 2 pointless: there was nothing left to collide with.) - Copy each
agents/<name>.mdto.cursor/agents/agentry-<name>.md(prefix avoids collisions with the user's own Cursor agents — seedecisions.mdD6). - For each
skills/<name>/SKILL.md, runtoCursorRuleand write to.cursor/rules/<name>.mdc. - For each
rules/<category>/<name>.md, runtoCursorRule(withlanguage-derived globs) and write to.cursor/rules/<category>/<name>.mdc(the category subdirectory is preserved). - If
mcp/has sources, write the samemcpServersmap to.cursor/mcp.json. It sits outside the wipedagents/andrules/namespaces and is never deleted — the same contract as.mcp.jsonandopencode.json(D20).
syncCodex() — runs in this order:
- Wipe
.codex/agents/skills/only. Does not wipe the parent.codex/(Codex storesconfig.tomlthere). - For each
skills/<name>/SKILL.md, runrenameSkill(content, "agentry-<name>")and write to.codex/agents/skills/agentry-<name>/SKILL.md. Copy any sibling files/directories alongsideSKILL.mdverbatim into the same target directory. - For each
agents/<name>.md, runagentToSkill(content, "agentry-<name>"); for each rule, runruleToSkill(...)(since v0.6). Both write to.codex/agents/skills/agentry-<name>/SKILL.md. - Commands, hooks, and MCP are skipped — see
decisions.mdD8 (commands) and D20 (MCP).
syncOpenCode() — runs in this order:
- Wipe
.opencode/agents/,.opencode/commands/,.opencode/skills/. Does not wipe the parent.opencode/(it may hold the user'sopencode.json). - For each
agents/<name>.md, runagentToOpenCodeAgentand write to.opencode/agents/<name>.md(no prefix — primitives map 1:1). - Copy each
skills/<name>/tree verbatim to.opencode/skills/<name>/. - For each
commands/<name>.md, runcommandToOpenCodeand write to.opencode/commands/<name>.md. - If
mcp/has sources, runtoOpenCodeMcpConfigand writeopencode.jsonat the repo root (mcpkey, translated shape). Written only when sources exist; never deleted. Rules and hooks are deferred.
In --dry-run mode, each file-helper logs [dry] <relative-path> and returns without touching the filesystem. The wipe step logs [dry] clean <relative-path>. Final line is Dry run complete. instead of Sync complete.. The sync flow is otherwise identical — directory iteration, frontmatter transforms, and target dispatching all run normally.
Running npm run sync twice produces a byte-identical tree. CI enforces this via git diff --exit-code after sync — see "CI" below.
Installers are POSIX shell (scripts/install.sh) and PowerShell (scripts/install.ps1). They are intentionally parallel: same flags, same logic, same source→destination mapping.
| Target | Scopes supported | Default scope | Source dir | Subdirs copied |
|---|---|---|---|---|
claude |
--user, --project |
--user |
.claude/ |
agents, skills, commands, rules |
cursor |
--project only |
--project |
.cursor/ |
agents, rules |
codex |
--user, --project |
--user |
.codex/agents/ |
skills |
opencode |
--user, --project |
--user |
.opencode/ |
agents, commands, skills |
| Target + scope | Destination |
|---|---|
claude --user |
$HOME/.claude/ (Unix) or $env:USERPROFILE\.claude\ (Windows) |
claude --project |
$PWD/.claude/ |
cursor --project |
$PWD/.cursor/ |
codex --user |
$HOME/.agents/ (Unix) or $env:USERPROFILE\.agents\ (Windows) |
codex --project |
$PWD/.agents/ |
opencode --user |
$HOME/.config/opencode/ (Unix) or $env:USERPROFILE\.config\opencode\ (Windows) |
opencode --project |
$PWD/.opencode/ |
cursor --user |
Rejected with an explicit error: Cursor has no user-level config dir. |
For each subdir in the target's subdir list:
- Skip if the source subdir does not exist in the repo.
- Create the destination subdir (
mkdir -p/New-Item -Force). - For each entry in the source subdir, copy to the destination, overwriting if it already exists. Subdirectories are removed first then copied recursively so stale files do not survive.
- Log each created path.
The script aborts if the generated source directory is missing — it prompts the user to run npm run sync first.
--uninstall (or -Uninstall on PowerShell) removes only entries whose names match entries currently in the repo's generated dir. User-authored files in the destination are preserved. This is name-based, not content-based: a file the user wrote that shares a name with an agentry file would be removed. Practically rare given the agentry- prefix on Cursor and Codex outputs, but worth knowing.
npm run lint validates every agent and skill. The lint script's behavior is:
- Frontmatter block must be present (lines opening with
---and closing with---). - All of
name,description,tools,modelmust be present and non-empty. namemust equal the filename without.md.descriptionmust be ≥ 20 characters.
SKILL.mdmust exist inside the skill directory.- Frontmatter block must be present.
- All of
name,descriptionmust be present and non-empty. namemust equal the skill directory name.descriptionmust be ≥ 20 characters.
- Commands frontmatter.
- Rules frontmatter.
- Body content rules (length, voice, marketing tone).
- Frontmatter on generated harness files — only sources are linted.
Tests are run by npm test using Node's built-in node:test runner. No external test framework is installed. 243 cases across seven files.
Five of the seven test the transform layer by importing a pure function. The two hook files cannot: a hook is a script whose contract is a process exit code, so those tests spawn the real script as a child process and assert on its status. Importing a function would not prove the hook works.
Covers parseFrontmatter, checkRequired, checkDescription:
parseFrontmatter— null when no frontmatter present; null when opening---has no closing; simple key/value pairs; CRLF line endings; body preserved with and without a leading blank line; empty body when content ends at closing---;rawreturns inner text without delimiters; array-shaped values preserved as literal strings; non-kv lines skipped; whitespace trimmed; blank lines inside the block tolerated; description value containing a colon parses correctly.checkRequired— all-present returns[]; missing keys reported; empty string treated as missing; output order matchesrequiredKeys; extra fields ignored.checkDescription— null for ≥ 20 chars; null at exactly 20;too shortfor shorter;missing or emptyforundefined/empty; customminLengthhonored.
Covers toCursorRule, globsForLanguage:
- No-frontmatter input → wrapped with new
alwaysApply: falseblock. - Frontmatter without
alwaysApply→ field appended. - Frontmatter with
alwaysApply: true→ preserved unchanged. - Frontmatter with
alwaysApply: false→ no duplicate added. - Indented
alwaysApplydetected (the detector uses/^\s*alwaysApply\s*:/m). - Blank line inserted between closing
---and body when source has none. - Existing blank line preserved.
- With-blank-line and no-blank-line sources produce identical output (normalization).
- Additional frontmatter fields (
tags, etc.) preserved verbatim. globsinjected beforealwaysApplywhen provided; not duplicated when already declared; null globs ignored; added in the no-frontmatter case; absent when no opts passed (skills unaffected).globsForLanguagemaps known languages case-insensitively and returnsnullfor unknown or missing.- CRLF invariance — a CRLF source produces the same logical output as its LF twin (no spurious blank line).
Covers renameSkill, agentToSkill, ruleToSkill:
renameSkill— name field updated; description and extra fields preserved; body verbatim; CRLF tolerated; null on no-frontmatter input; fields starting withname(e.g.namespace) untouched.agentToSkill—toolsdropped;modeldropped;nameset to new value; description preserved; body verbatim including code blocks and headings; output is a valid SKILL.md structure; extra agent-only fields dropped; null on no-frontmatter input.ruleToSkill—languagedropped, name/description kept; body verbatim; description falls back to the first heading, then a generic label; never returnsnull.- CRLF invariance —
renameSkill,agentToSkill, andruleToSkillproduce the same logical output for a CRLF source as its LF twin (no spurious blank line).
Covers agentToOpenCodeAgent, commandToOpenCode:
agentToOpenCodeAgent—mode: subagentset anddescriptionkept;name/tools/modeldropped; body verbatim including code blocks; a colon-containing description preserved; null on no-frontmatter input.commandToOpenCode—descriptionkept andargument-hintdropped;$ARGUMENTSpreserved in the body; null on no-frontmatter input.- CRLF invariance — both transforms produce the same logical output for a CRLF source as its LF twin.
Covers validateServer, toMcpServersJson, toOpenCodeMcpConfig:
validateServer— accepts stdio (command + args), remote (url), and command-plus-env servers; rejects no-transport, empty command, non-array args, non-object env, null, array, and primitive inputs; reports multiple problems at once.toMcpServersJson— wraps undermcpServers; preserves the definition verbatim; sorts by name; output independent of input order; does not mutate the caller's array; 2-space indent with trailing newline; empty list yields an empty map.toOpenCodeMcpConfig— wraps under$schema+mcp; maps stdio totype: localwith acommandarray and foldsenvintoenvironment; maps remote totype: remotewithurl+headers; sorts by name; trailing newline.
Spawns hooks/protect-generated-dirs.js and asserts on its exit code (0 allow, 2 block).
- Every generated subdirectory blocks — including the three
.opencode/ones, which went unprotected from v0.7.0 until the hook was corrected. - Source files, the three MCP config files, and per-user harness state (
.claude/settings.local.json,.cursor/environment.json,.codex/config.toml) are allowed — sync preserves them, so a hand-edit is legitimate. - Windows separators and absolute paths normalize;
notebook_pathis read alongsidefile_path. - Each block message names the correct source file to edit instead.
- Empty stdin, malformed JSON,
{}, a missingtool_input, and a nullfile_pathall exit0— fail open.
Characterizes the other six guards, pinning both what they block and what they deliberately do not, so a later edit to a regex or a pathspec parser cannot silently widen or narrow a guard.
block-no-verify,block-force-push,guard-dangerous-bash,protect-lockfile-edit,secret-scan-on-edit— table-driven block/allow pairs, including the boundaries called out in the Hook reference (a feature branch may be force-pushed;rm -rf ./buildis not catastrophic).block-secret-file-stageresolves pathspecs by asking git, so it is exercised against a real temporary repository holding a real.envandserver.key: an explicit add, the sweepinggit add .and-A,--dry-runstaging nothing, and a gitignored file ceasing to be flagged.- All six are asserted to fail open across seven malformed-payload shapes — 42 of the cases here.
| Script | Command | Purpose |
|---|---|---|
sync |
node scripts/sync-harnesses.js |
Regenerate all harness directories from source. |
sync:dry |
node scripts/sync-harnesses.js --dry-run |
Log what sync would do without touching the filesystem. |
doctor |
node scripts/doctor.js |
Report installation health. |
lint |
node scripts/lint-frontmatter.js |
Validate frontmatter on agents and skills, plus MCP server JSON. |
test |
node --test |
Run unit tests (built-in discovery finds tests/*.test.js; no path arg, so it works across Node 20 and 24). |
Node engine requirement: >=18.0.0. No runtime dependencies; no devDependencies.
Patterns and rationale:
node_modules/,npm-debug.log*,yarn-debug.log*,yarn-error.log*— standard Node ignores (project has no deps, but contributor environments may install one transiently)..DS_Store,Thumbs.db— OS metadata..vscode/,.idea/— editor settings..claude/settings.local.json— Claude Code per-user state (permissions cache). The rest of.claude/is tracked; this single file is not.
Generated directories .claude/, .cursor/, .codex/, .claude-plugin/ are tracked deliberately — contributors browsing the repo on GitHub should see what sync produces. The cardinal rule "do not edit generated dirs" is enforced by convention, not by .gitignore.
Three jobs, all on ubuntu-latest with Node 24, running on pushes and PRs to main and dev:
sync-determinism—npm run sync, thengit status --porcelainfails the build if sync produced uncommitted changes. Catches the common contribution mistake of editing source without committing regenerated harness files.frontmatter-lint—npm run lint. Catches malformed agent/skill frontmatter.tests—npm test. Runs the 243 unit tests.
Scoped to permissions: contents: read — no job here writes a comment, a commit, or a release. Tests do not block sync-determinism or lint; the three jobs run in parallel. There is no functionality test that exercises the actual sync output against a fixture — sync is verified only by its determinism and by the lint pass on its input.
Triggered by a v* tag push. Needs permissions: contents: write because it creates a Release.
- Verify the tag matches
package.json— avX.Y.Ztag whose version disagrees with the manifest fails here. - Gate on the same three checks —
npm run syncmust produce zero drift, thennpm run lintandnpm test. - Extract the CHANGELOG section for that version, and
gh release createwith it as the notes.
Two things to know before tagging. A tag push runs the workflow file as it existed at the tagged commit, so a tag older than this workflow fires nothing and its Release must be created by hand. And only the last commit of a release cycle is sync-clean: chore(release): X bumps the version while the regenerated output lands in the following chore(sync): regenerate harness outputs for X. Tag that one.
None. The scripts read no environment configuration beyond what their CLI flags specify.
README.md— project overview, installation, status, roadmap.CONTRIBUTING.md— contribution workflow, conventional commits, rejection criteria.CLAUDE.md— AI assistant operating guidance for the repo.architecture.md— adapter pattern, sync engine design, source-of-truth layout, settings preservation, Codex adapter narrative.authoring.md— step-by-step authoring of agents, skills, and rules.decisions.md— numbered design decisions (D1–D20) with rationale, alternatives, and revisit triggers.../CHANGELOG.md— release history and deferred work.