Skip to content
Back to skills

Docs

ASecurity

This is the contract a skill must meet. The `change-skill` repository skill (`.claude/skills/change-skill/SKILL.md`) is the procedure for meeting it, and `python tools/new_skill.py <name> --description "<description>"` scaffolds a new skill's frontmatter, metadata, and reference section; run it with `--help` for the other options.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 19, 2026
ai-agentsjavascripttypescriptpythongojavashellbashnodeexpressdebugging

Works with

  • claude code
  • terminal
  • cli
  • api
  • mcp

Security analysis

A100/100

Pro scans all 17 files and shows the line behind each finding

Scanned October 19, 2026

npx -y skills add EJRyan89/coding-agent-skills --skill docs --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Docs?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Docs
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/ejryan89-docs/badge)](https://www.skillsdirectory.com/skills/ejryan89-docs)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
# Adding a skill

This is the contract a skill must meet. The `change-skill` repository skill (`.claude/skills/change-skill/SKILL.md`) is the procedure for meeting it, and `python tools/new_skill.py <name> --description "<description>"` scaffolds a new skill's frontmatter, metadata, and reference section; run it with `--help` for the other options.

## Files

Add each skill as a pair:

```text
skills/<skill-name>/SKILL.md
deploy-meta/<skill-name>.json
```

The `name` in the `SKILL.md` frontmatter must exactly match the directory and metadata filename. Skill and shared-asset names must be unique. The skills directory is flat: the deployer refuses a `SKILL.md` anywhere but directly under `skills/<skill-name>`, so group skills with a bundle (see [Opt-in skills and bundles](#opt-in-skills-and-bundles)), not a folder.

The deployer and the repository tools read `name`, `description`, `argument-hint`, and the invocation flags with `frontmatter.py` from the hidden `skill-core` skill. Each of those may be a plain, quoted, or block (`|` or `>-`) scalar, but not a list or a nested mapping. Deployment stops with the reason when `name` cannot be read, and validation when any of them cannot. Keys they do not read, such as `hooks`, are never parsed. A skill script that reads frontmatter imports the same reader as `frontmatter`, declaring `skill-core` like its `console.py`.

Claude Code loads every model-invocable skill's description into every session, and Codex and GitHub Copilot CLI read it through the skill's runtime adapter. Keep it within 1,024 characters once rendered: Copilot refuses a longer one, so deployment stops instead, and `tools/new_skill.py` refuses one before it writes anything. Add `disable-model-invocation: true` to a skill only the user should start, such as one that deletes branches or redeploys, and also `user-invocable: false` to an internal dependency nobody starts directly. Never add either to a skill another skill invokes by name; a contract test checks this.

Skill and bundle names must:

- be at most 64 characters, the limit for a skill `name` in the Agent Skills format;
- use only lowercase letters, digits, and hyphens, without a leading or trailing hyphen, such as `repo-cleanup` or `review-prs`;
- not be a reserved Windows device name such as `con`, `aux`, `nul`, `com1`, or `lpt1`.

The Agent Skills frontmatter rules ("YAML frontmatter requirements" in Anthropic's [skill authoring best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices)) add two more:

- a skill name must not contain the reserved words `anthropic` or `claude`, so `claude-helper` is refused;
- neither a skill's name nor its description may contain an XML tag such as `<tag>` or `</tag>`. The naming grammar already keeps angle brackets out of a name; in a description, a bare `<` or `>`, as in `a < b` or `x -> y`, is not a tag and is allowed.

The deployer rejects a name that breaks these rules before rendering anything, and a description with an XML tag once rendered, before changing any file. `tools/new_skill.py` refuses the same values.

When a skill needs executable logic, place both its implementation and tests under:

```text
skills/<skill-name>/scripts/
```

A skill's executable files are Bash, Python, and PowerShell: `.bash`, `.sh`, `.py`, and `.ps1` files, run by `bash`, `python`, and `pwsh`, the commands `deployer/tools.py` lists, and rejected anywhere else in a skill. JavaScript and TypeScript are not supported, because no runtime they need is a deployment prerequisite and the renderer cannot validate them, and neither are PowerShell modules and Windows batch files, which no analyzer or command policy reads: `.js`, `.mjs`, `.cjs`, `.ts`, `.psm1`, `.cmd`, and `.bat` files are rejected anywhere in a skill, `scripts/` included, and so is a file that starts with `#!` without one of the extensions above. Each script sits directly in `scripts/`, never in a folder below it, which the script policies do not read. Keep `SKILL.md` and supporting Markdown focused on orchestration and explanation. Executable-language fences are reserved for short command examples of at most five lines; extract longer logic into `scripts/`.

The body keeps what the agent needs to run the skill: the commands, what their output means, and what to ask the user. What a person reads once, such as guarantees the scripts enforce, a glossary of output that labels itself, or a procedure for an action the skill never takes, belongs in the skill's section of [skills.md](skills.md) instead. For structure, follow Anthropic's [skill authoring best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices): keep the body under 500 lines, link every reference file directly from `SKILL.md`, and start a reference file over 100 lines with a table of contents. `analyze-skill-cost` checks all of this, by content rather than by size.

### Description

A model-invocable skill's description is all the model reads when deciding whether to start it, so it says what the skill does, then when to use it: `<What it does>. Use it when <the requests and situations that call for it>.` Name the requests the way a user would make them and, where it helps, a near miss the skill should not take. Keep what it does first, and add trigger cues rather than length, since every session loads the description. A user-only skill's description never starts anything, so it needs no such clause.

`tools/new_skill.py` refuses, and validation fails for, a model-invocable skill whose description has no `Use it when`, `Use when`, `Use it before`, or `Use it after` clause.

### Granting tools

In Claude Code, `allowed-tools` pre-approves the listed tools for the turn that starts the skill, so they run without asking the user. It restricts nothing: a tool it leaves out still runs after the usual prompt. Codex ignores the field, and the adapters do not carry it to Copilot, which cannot scope a shell grant (see the end of this section).

Grant shell commands as patterns, never as a bare `Bash` or `PowerShell`, which would pre-approve every command in that turn:

```yaml
allowed-tools: ["Bash({{PYTHON}} -B \"${CLAUDE_SKILL_DIR}/scripts/widget_report.py\" *)", "PowerShell({{PYTHON}} -B \"${CLAUDE_SKILL_DIR}/scripts/widget_report.py\" *)", "Read"]
```

- Grant each pattern to both shells. On Windows the model may run a fence's command through its PowerShell tool rather than Bash. A Bash-only grant then prompts, and in a non-interactive run the command is denied.
- Write the pattern the way the command is written, `{{PYTHON}}` and opening quote included, so the grant and the command render alike. Claude Code fills in `${CLAUDE_SKILL_DIR}` in the pattern as in the command, then compares the command's text, quotes and all, with `*` standing for any text. The same pattern without the quote never matches a quoted command. A skill that runs a sibling's scripts grants each one it runs by name, ending at the closing quote, such as `Bash({{PYTHON}} -B "${CLAUDE_SKILL_DIR}/../code-review-core/scripts/review_pipeline.py" *)`, never the sibling's folder.
- Grant each script the body runs by name, its own as well as a sibling's, the pattern ending at the script's closing quote, a space, and `*`. A pattern that runs an unquoted path, such as a repository skill's `python tools/new_skill.py *`, ends the path at a space before the `*` instead. A `*` with a space before it at the end also matches the command with no arguments.
- A grant is a prefix match on the command's text, not a sandbox: Claude Code normalizes no path in a shell grant, so any command that starts with the pattern's text is approved whatever follows. It does check each command of a line that joins several with `&&`, `||`, `;`, or `|` on its own, so a grant covering the first command approves none of the rest, and the inventory reports the rest as ungranted. A pattern whose `*` continues a path, such as `scripts/*` or `tool.py*`, therefore also matches `scripts/../../<other skill>/scripts/<script>.py`, which reaches every installed skill's scripts, whose arguments the granting skill never checks. A pattern that ends at a script's closing quote, `.../tool.py" *`, matches that script with any arguments and no `..` path, because the quoted path ends before the `*`. The script itself, together with Claude Code's working-directory and protected-path rules, bounds what the command does, so a script that acts on a path the user supplies validates it.
- A repository tool that writes into the checkout, such as `tools/skill_shims.py`, takes `--root <dir>` to work on another checkout, as its regression suite does, and a `*` after the tool's path covers that option too. Such a tool parses `--root` with `checkout_root` in `tools/checkout.py`, which refuses a directory that does not hold `source.json` and `.claude/skills`, or that lies in a skills directory such as `~/.agents/skills`, whose adapters the deployer owns. Grant a tool whose arguments a step fixes by the forms the step runs, such as `Bash(python tools/skill_shims.py --write)`, with no `*`.
- Keep shell variables and command substitution, such as `"$PWD"` or `$(...)`, out of fence commands: Claude Code asks before it runs a command that expands one, whatever is granted. Have the script default to the current directory instead.
- Grant any other command exactly as the body runs it, such as `Bash(git rev-parse --show-toplevel)`, or, for `git` or `gh`, as the subcommand it runs, such as `Bash(gh pr create *)`. Leave a command that runs code from the target repository ungranted, so the user approves it.
- Never end a pattern's text before its `*` at a program and its switches, such as `python *`, `bash -c *`, `pwsh -Command *`, or `git *`: it pre-approves any code or subcommand the `*` supplies. Before the `*`, a pattern names a script by its path, as the program or after the program and switches that take no value, written as capital letters such as `-B`, or names a `git` or `gh` subcommand right after the program. A pattern with no `*` approves only the command it names.
- Grant a pattern only for a command the body shows, in a shell fence or a code span. A fence counts wherever Markdown allows one, indented, in a list item, or in a blockquote, and under any command shell's label, such as `bash`, `zsh`, or `powershell`. A code span in which a program runs a script is held to the rules above as a fence command is: a grant must cover it, and must not cover one that runs code from the target repository. A pattern no command in the body matches pre-approves something the skill never asks for, such as a command a generic procedure leaves to the repository it works in, which should prompt there.
- A hidden skill (`disable-model-invocation: true` and `user-invocable: false`) grants nothing. No turn starts it, so its grants approve nothing; the skills that run its scripts grant them.
- A script of the skill's own that runs code from the target repository, such as one that starts the MCP servers the repository configures, says so as a module-level `RUNS_REPOSITORY_CODE = "<reason>"`. Leave its command out of the grants, which name the skill's other scripts, such as `Bash({{PYTHON}} -B "${CLAUDE_SKILL_DIR}/scripts/audit_ai_config.py" *)`.
- List tools other than the shells by name, such as `Read` or `AskUserQuestion`, and only those a step uses. Each grant widens what runs unasked (a `Read` grant covers files outside the working directory, which otherwise prompt), so name its use in the step that needs it, such as "read the `LOG` file" or "every file the glob `docs/**/*.md` matches".

These rules were checked by hand in Claude Code on Windows; the runtime canary behind the README's tested versions does not recheck each one. Validation fails when a skill grants a shell every command, grants a pattern to one shell only, grants a sibling skill's folder rather than one of its scripts by name, grants a pattern whose `*` continues a path rather than following a space outside quotes, such as its own `scripts/*` or `tool.py*`, grants a pattern whose text before its `*` names a program but no script or `git` or `gh` subcommand, such as `python *`, grants a tool that no step names or implies (a prohibition such as "do not read", or a prompt quoted for a subagent, implies nothing), runs one of its own or a sibling's scripts that a shell's grants do not cover (unless the script declares `RUNS_REPOSITORY_CODE`), grants a command that runs a script declaring it, expands a shell variable in a command, grants a scoped shell pattern that matches no command in the skill's fences or code spans, or is a hidden skill with any grant. It applies the `analyze-skill-cost` inventory (`skill_inventory.py tools`), which reports these as `UNSCOPED_ALLOWED`, `UNPAIRED_ALLOWED`, `SIBLING_FOLDER_ALLOWED`, `OPEN_PATH_ALLOWED`, `NO_SCRIPT_ALLOWED`, `UNUSED_ALLOWED`, `UNGRANTED`, `GRANTED_REPOSITORY_CODE`, `EXPANDS`, `IDLE_ALLOWED`, and `HIDDEN_ALLOWED`, and reports a declared script's command left to prompt as `REPOSITORY_CODE`. It reads fences through `find_fences` in skill-core's `markdown_sections.py`, the reader the deployer renders with, so the audit, validation, and rendering agree on what a fence is. It also fails a `RUNS_REPOSITORY_CODE` that is not one module-level non-empty string, or that sits in a script with no process-starting call. The shared command reader in `tests/validation/skill_scripts.py` recognizes those calls through imports and single-binding callable aliases in their lexical scopes, including the skill-core clients; imports, command lookups, and command lists alone do not count. Unresolved callable aliases and commands whose program cannot be read fail rather than being guessed. Validation also fails a repository skill's grant whose `*` covers the `--root` option of a `tools/` script that does not parse it with `tools/checkout.py`'s `checkout_root`. A `tools/` module that starts Claude Code with `--allowedTools`, as `tools/skill_evals.py` does, is held to the same pattern rules for each grant it passes, which pre-approve those commands for the whole session: each value after the option is a string or an f-string holding one whole grant, or a starred call of a function the module defines whose every string but its docstring is one, a value an f-string fills in is read as its name, so the module fills in no text that holds a `*`, and validation fails a value it cannot read that way. Starting a process alone does not require the declaration: it names running code from the target repository, not every external command.

The runtime adapters leave `allowed-tools` out because Copilot CLI cannot scope a shell grant to a skill's scripts. This was checked with Copilot CLI on Windows, running a probe skill from a directory whose path has a space, with no shell grant on the command line:

- Copilot accepts the field and reads `Bash(...)` as its own `shell(...)`, but it matches a shell grant against the command's name, such as `python`, not against the command's text. A grant naming the script path, such as `shell(python -B "<skill directory>/scripts/*)`, approved nothing, quoted or not, and neither did the same pattern passed as `--allow-tool`.
- The only grant that let the script run, `shell(python:*)`, also ran a `python -c` command outside the skill.
- A non-interactive `copilot -p` run applied none of a skill's grants, not even `shell(python:*)`.

Carrying the field would approve either nothing or every Python command, so a Copilot user approves each script a skill runs. Revisit this when Copilot matches a shell grant against the command's arguments.

### Paths to a skill's own files

A skill names its own files, and a sibling skill's, through `${CLAUDE_SKILL_DIR}`, never through a rendered install path such as `{{HOME}}/.claude/skills/<name>/`. As a skill's Markdown writes it, in a Bash fence before the deployer renders `{{PYTHON}}`:

```text
{{PYTHON}} -B "${CLAUDE_SKILL_DIR}/scripts/tool.py" report
{{PYTHON}} -B "${CLAUDE_SKILL_DIR}/../code-review-core/scripts/review_pipeline.py" check --run "<run>"
```

Claude Code replaces `${CLAUDE_SKILL_DIR}` with the absolute directory of the skill it loads. Codex and Copilot read the skill through its adapter, which states the same directory for them (see the adapter paragraph below). Either way the path is absolute when the command runs, so it does not depend on the working directory, and the skill works under any install layout. Keep the quotes: the directory may contain spaces.

- Name a sibling as `${CLAUDE_SKILL_DIR}/../<skill>/` and declare it in `skill_deps`, so it is installed beside the skill.
- Never write a bare `scripts/...` or `../<skill>/...` path in a shell fence; it resolves against whatever directory the session is in.
- In `SKILL.md` prose, name the skill's own files in a code span as `${CLAUDE_SKILL_DIR}/scripts/...` or `${CLAUDE_SKILL_DIR}/references/...`, never bare. Claude Code resolves a bare path against the working directory, costing a failed read and a search, and Codex and Copilot resolve it against the adapter, which has neither folder. Files under `references/` are exempt, because Claude Code fills in the variable only in `SKILL.md`. Validation fails on a bare path in a `SKILL.md` code span outside a fence.
- Claude Code replaces the variable everywhere in the skill, prose included, so do not use it as literal text, for example to explain it.
- Claude Code replaces it only in a skill it loads, so a skill that needs another skill's procedure invokes that skill by name, as `update-pr-dashboard` invokes `review-prs`; it never reads the other skill's `SKILL.md` or cites its step numbers or section titles, which break silently when that skill is renumbered or retitled.
- A script that needs the user's Claude directory derives it, as `curate-agent-memory` does from `CLAUDE_CONFIG_DIR` or `~/.claude`, rather than taking a rendered `{{HOME}}`. Keep `{{HOME}}` for documentation that names another location, such as the user's skills root.

Repository validation fails when a skill or agent names any skill through `{{HOME}}/.claude/skills/<skill>`, when a shell fence runs a script or a `SKILL.md` code span names its own file by a bare relative path, when a skill reaches `../<skill>` without declaring it in `skill_deps`, when a script puts another skill's `scripts` directory on `sys.path` without declaring that skill in `skill_deps`, or when a skill reads another skill's `SKILL.md` or names it beside a step number or section title.

### Script results

Every skill script reports the same way, so a `SKILL.md` reads each one alike:

| Exit | Means | Prints |
|---|---|---|
| 0 | The command did its job, including "nothing to do" and a state the skill polls again, such as `RUNNING`. | Its result lines. |
| 1 | Findings the user must act on, such as violations or invalid results, or a named failure. | Its finding lines, or a last line `FAILED <reason>`. |
| 2 | A usage error: arguments `argparse` rejects, including its `type=` checks, before any work starts. | `argparse`'s usage text on stderr. |
| 130 | Ctrl+C stopped the script. | A last line `FAILED interrupted`. |

- Every line goes to stdout, one fact per line, as `KEY value…`. A failure is a `FAILED <reason>` line on stdout, never on stderr and never through `parser.error`, which is for usage alone. A batch command names each item's failure on its own line, such as `REPOSITORY_FAILED <repository> <reason>`, and exits 1.
- An expected error, such as a missing file, a failed `git` or `gh` call, or a directory that is not a repository, becomes a `FAILED` line, never a traceback: `main` catches it and prints its own line. An error `main` did not foresee becomes one too, through the runner every entry point starts with (below): a last line `FAILED <type>: <message>`, the exception's type and its message on one line, and exit 1.
- A script runs `gh` only through `GitHubClient` in the hidden `skill-core` skill's `github_client.py`, declared and imported like its `console.py`, and declares `gh` in `tools` (or `optional_tools`). Every gh command reads no stdin, shows no prompt, and fails when it runs longer than the client's `timeout` (300 seconds unless the client names another). The client classifies a failure into `GitHubError.kind` (`prerequisite`, `execution`, `timeout`, `authentication`, `forbidden`, `not_found`, `rate_limit`, `search_cap`, `network`, `sso_partial`, `malformed` for JSON it cannot parse, `unknown_outcome` for a request that may have changed GitHub before a transient failure lost its answer, or `api` for anything else), and a caller's parse raises the same error with its own kind, such as `malformed` for an unexpected shape, `incomplete` for results GitHub cut short, `input` for an argument the caller rejects, or `ineligible` for a pull request it will not take. The client waits out a rate limit with one bounded backoff policy, and retries a read a few times after a transient failure (a 502, 503, or 504, or a connection dropped mid-request), unless the call passes `retry=False`; it never retries a request that may change GitHub, and treats any command it does not recognize as a read as such a request. It replaces each byte of output that is not UTF-8 with U+FFFD, counting them in `CommandResult.replaced`. A regression suite injects a runner that returns `CommandResult`, and a sleeper that records the waits. `json` returns the parsed output, and a caller's own parse for `request` starts from `json_body`, the same parse, so output that is not JSON fails as `malformed` everywhere.
- A Python script runs `git` only through `GitClient` in the hidden `skill-core` skill's `git_client.py`, declared and imported like its `console.py`. Every git command reads no stdin but the bytes a call gives in full as `input_bytes`, shows no prompt (`GIT_TERMINAL_PROMPT=0`, `GCM_INTERACTIVE=never`), and fails when it runs longer than the client's `timeout` (300 seconds unless the client or the call names another), so an expired credential fails a command instead of waiting on a prompt. A `fetch` or `ls-remote` that fails on a transient transport fault, such as a dropped connection or a host briefly not found, is retried a few times with short waits inside the same time limit, and the last attempt's answer is the result, with the retries it gave up after named on its stderr; `push` and every other subcommand run once, and a regression suite replaces the waits with the client's `sleeper` and `clock`. `run` returns a `GitResult` with the exit status, stdout, and stderr whatever git's exit, since an exit status is often the answer; `output` returns stdout and raises on a nonzero exit. stdout keeps every byte (`GitResult.output_bytes()` gives them back), and stderr replaces a byte that is not UTF-8 with U+FFFD. `stream` runs a command that answers requests while it runs, such as `cat-file --batch`; there the timeout bounds each wait for output, so a long answer that keeps arriving never times out. A failure raises `GitError` with its class in `kind`: `prerequisite`, `execution`, `timeout`, `not_repository`, or `git` for anything else. A regression suite injects a runner that takes the command and its timeout and returns `GitResult`, and, for a call given `input_bytes`, an `input_runner` that also takes them by that keyword; a client given a runner alone refuses such a call rather than run real git. `rev_parse_path` gives the absolute path `git rev-parse` names, such as `--show-toplevel` or `--git-common-dir`, or `None` outside a repository or when git cannot answer. Validation fails on a Python script outside `skill-core`, tests aside, that runs `git` or `gh` itself: a list or tuple that begins with either, or a call that starts a command, such as `subprocess.run` under any name its imports give it, given either as a string or through a name bound to one.
- A script prints no JSON. Data the next step reads goes to a file whose path the script prints on one line (see "Working files"). A report the skill shows the user as it is, such as `github-activity-report`'s table, stays a report.
- No other exit codes. A `SKILL.md` keys on the printed lines, not on which nonzero code came back.
- Output is UTF-8, and no script ends in a traceback. Every entry point's `if __name__ == "__main__":` block is one call to `run_main` from the hidden `skill-core` skill's `console.py`: `run_main(main)`, or `run_main(lambda: main(sys.argv[1:]))` for a `main` that takes its arguments. The entry point declares `skill-core` in `skill_deps` and imports it as "Validation" below shows. `run_main` first sets stdout and stderr to UTF-8, because a Windows pipe defaults to a legacy code page that cannot encode every path or title a script prints. It then runs `main` and exits with what `main` returns. A `SystemExit`, `argparse`'s usage error included, passes through; any other exception prints `FAILED <type>: <message>` and exits 1; Ctrl+C prints `FAILED interrupted` and exits 130. Neither prints a traceback unless the entry point passes `traceback_requested`, a function asked only then, which says to: `deploy.py` passes one that reads `--debug` and `DEPLOYER_DEBUG`, and the traceback follows the line on stderr. Pass it `errors="replace"` for output that may hold surrogate-escaped paths, or `newline="\n"` for line endings that must not follow the platform. To test the encoding, run the script with `PYTHONIOENCODING=cp1252` on output that code page cannot encode and compare the UTF-8 bytes it printed, not only its exit code: an encoding failure exits 1 too.

A script that must speak another protocol states why beside its code as a module-level `EXIT_CONTRACT_EXEMPT = "<reason>"`: `review_guard.py`, for example, answers Claude Code's hook protocol. Repository validation fails when any other script calls `parser.error` while handling an exception, prints `FAILED` on stderr, prints `json.dumps` output, or exits with a literal code other than 0, 1, or 2. It also fails when an entry point's `__main__` block (a skill script's, `deploy.py`'s, or a deployer or `tools/` module's) does not start with `run_main()`, resolved through its imports, or when a script outside `skill-core` reconfigures a stream's encoding itself, and it runs each entry point's block as written with its `main` failing, to show the `FAILED` line and the exit code.

### Working files

A script that writes a working file, such as a batch, an input, or a plan, chooses the path itself: by default a new directory from `tempfile.mkdtemp` with a prefix naming the skill, such as `review-prs-batch-`. It prints the path on one line, such as `BATCH <file>`, and the next step reads that line. An option for an explicit path may stay, but the script refuses, with a `FAILED` line and exit code 1 before writing anything, any path inside the skills directory it runs from, `~/.claude/skills`, or `~/.agents/skills`: `skills_directory_holding` in the hidden `skill-core` skill's `skill_roots.py` names the one a path lies in.

Never leave the path to the agent. Given a placeholder such as `--output "<file>"` and a skill directory it already knows, an agent writes beside `SKILL.md`, and the deployer then sees the installed skill as modified and skips it on every later update. Repository validation fails when a command fence in a skill passes a `<...>` placeholder to `--output`, `--output-<name>`, `--out`, `--out-dir`, or `--plans`. A placeholder for a path an earlier command printed, such as `--run "<run directory>"` or `--input "<input file>"`, is fine.

### Metadata

`deploy-meta/<name>.json` is the skill's deployment metadata: what the deployer needs to know to install the skill, which the runtime never sees. It lives outside the skill directory because the deployer renders, installs, and hashes only what is under `skills/<name>/`, so a deployed skill carries no deployer-specific file, and a change to how a skill is deployed never touches its template or its hash.

`deploy-meta/<name>.json` declares the variables and shared files needed by the skill:

```json
{
    "required_vars": ["REPOS_ROOT"],
    "shared_deps": ["runtime-compatibility.md"]
}
```

Metadata may also declare same-source skill dependencies and selection visibility:

```json
{
    "required_vars": [],
    "shared_deps": ["runtime-compatibility.md"],
    "skill_deps": ["code-review-core"],
    "selectable": true
}
```

`selectable` defaults to `true`. Set it to `false` only for an internal support skill that is reachable from a selectable skill. Dependencies must exist in this source and form an acyclic graph. The deployer expands the complete transitive dependency closure before rendering or mutation.

Every skill declares `runtime_support`, which runtimes run it, one value per runtime (`claude-code`, `codex`, and `copilot-cli`): `"full"`, or an object whose `level` is `partial` or `none`, whose `needs` names the capabilities the runtime lacks for it, and whose `reason` says in one line what the user loses:

```json
"runtime_support": {"claude-code": "full", "codex": "full", "copilot-cli": {"level": "partial", "needs": ["user-only-start"], "reason": "A headless copilot -p session cannot start it; start it from an interactive session."}}
```

The capabilities, and which runtimes offer them, are catalogued in `deployer/runtime_support.py`: `agent-delegation` (starting a subagent; every runtime), `workflow` (Claude Code's Workflow tool), `fail-closed-agent-hooks` (running a subagent under the hooks its agent definition declares, with a tool call blocked when a hook fails or times out, which the code-review reviewer's guard needs; Claude Code, since Codex and Copilot CLI run hooks but let a call through when one fails), `user-only-start` (starting a user-only skill from a headless session as well as an interactive one; Claude Code and Codex), and `sandbox-network` (network access for a skill's commands in the runtime's default sandbox; Claude Code and Copilot CLI, since Codex's default sandbox turns it off). The value follows from what the skill needs, and validation derives that from its frontmatter: an `Agent` grant in `allowed-tools` needs `agent-delegation`, a `Workflow` grant needs `workflow`, an `Agent` grant in a skill whose `SKILL.md` names, in backticks, an agent under `agents/` whose definition declares `hooks` needs `fail-closed-agent-hooks`, `disable-model-invocation: true` needs `user-only-start`, a network tool (`gh` or `copilot`) among the `tools` or `optional_tools` in `deploy-meta/<name>.json` needs `sandbox-network`, and a skill another skill's `SKILL.md` says to invoke passes on what it does, though not how it starts. A runtime that offers every need is `full`; one that lacks any is `partial` or `none` and names exactly what it lacks. A hidden skill is `none` on every runtime, with the reason that no runtime starts it. The deployer refuses a malformed declaration, an unknown runtime or capability, and a `partial` or `none` without a reason. `tools/new_skill.py` writes the value that follows from the options it is given, with a default reason for each partial to sharpen. `tools/skill_reference.py` prints the declarations as "Runtime support" in `docs/skills.md`, and the runtime canary compares each run with them.

Every metadata file has one layout, the one `tools/new_skill.py` writes: one key per line, indented four spaces, with each value on its key's line. Repository validation fails on any other layout.

### Commands skills may run

Skills run on machines where installing extra utilities may be inconvenient or not allowed, so a skill may run only:

- Python and its standard library. Call it as `{{PYTHON}}` in a Markdown example and a grant, which the deployer renders as `python` on Windows, where `python3` is often a Microsoft Store placeholder, and as `python3` elsewhere, where a distribution may install no `python` command; see [Template values](#template-values).
- `git`, Bash, and the utilities Git for Windows bundles on Windows and a Linux system provides, such as `grep`, `sed`, `awk`, `find`, `tr`, `xargs`, and `curl`. On Windows they come with Git for Windows, together with `cygpath`; on Linux they are the system's.
- PowerShell: `pwsh`, and on Windows also Windows PowerShell (`powershell`).
- A tool from the catalogue in `deployer/tools.py`, declared in the skill's metadata as described below.

`STANDARD_COMMANDS` in `deployer/tools.py` lists the first three groups exactly; the platform-specific names among them (Python's command, and on Windows `powershell` and `cygpath`) come from `deployer/platform_support.py`. Repository validation fails when a skill's scripts or its Bash or PowerShell examples run anything else. It reads a Python script through `ast`, resolving each call through the script's imports and simple assignment aliases in its lexical scope, following a command held in a name to the literal the script binds it to in that scope, and reading skill-core's `run_bounded`, `streaming`, `GitClient`, and `GitHubClient` as the commands they run, and `os`'s exec and spawn calls, `os.startfile`, `pty.spawn`, and an event loop's `subprocess_exec` and `subprocess_shell` on any receiver as the program they start. A module the script reaches by its name, through `importlib.import_module`, `__import__`, or `sys.modules`, is read as if imported, and a runner taken from a module looked up by a computed name, or from a dict or tuple indexed where it is written, fails as a runner handed on as a value does. Validation reads a `.ps1` script or a PowerShell fence through PowerShell's own parser, where the aliases and commands PowerShell ships are standard. An alias binds a name once to a known `Name` or `Attribute` chain: by an assignment such as `runner = subprocess.run`, an assignment expression, an item of a literal tuple or list it is unpacked from or loops over, a parameter's default, or a case that captures its whole subject. A call through an alias that is rebound or cannot be resolved fails validation. Dynamic callable construction, such as a call's result or an item of anything but a literal, is not an alias.

Validation reads a Python command's program wherever the script names it: the first item of a list or
tuple, the first word of a string, or the literal prefix of an f-string, a `%` format, or a `.format()` string
before its first placeholder, so `"gh api %s" % endpoint` runs `gh`; what a `" ".join`, a `shlex` split or join,
a path conversion, or a lookup such as `shutil.which` is given; what one of the script's own functions returns;
`sys.executable`, which is the platform's Python command; and a runner's `executable=`, which replaces the program. A command a
function takes as a parameter is read at every call of that function in the script. A command whose program it
cannot read fails both the declared-tool and shared-client policies instead of being ignored: a format with no
literal program prefix, such as `f"{program} api"`, a value the script reads from elsewhere, or a parameter of a
function no call in the script passes, that the script hands on as a value, or that other scripts may call by its
public name. Keep the program name literal and interpolate only its arguments. A function whose program truly
comes from elsewhere, such as a runner a client injects or a server a repository configures, states why beside
the code, as a module-level `COMMANDS_ALLOWED = {scope: reason}` keyed by the function's qualified name or by a
module-level assignment's target; validation fails on an allowance that sanctions nothing it reads. A parameter
that `COMMAND_RUNNERS` in `tests/validation/skill_scripts.py` names, such as `run_bounded`'s command, is read at
each caller instead.

A command that runs another reads as both. In Bash, validation reads the program each command substitution, quoted
or not, runs, and the program a wrapper runs: `xargs`, `env`, `command`, `exec`, `nohup`, `timeout`, `nice`,
`stdbuf`, and `find`'s `-exec`, and every command of the script `eval` or a shell's `-c` runs; `command -v` only looks
one up. In PowerShell it reads what `Start-Process` starts, what `& (Get-Command <name>)` runs, and every command of
the string `Invoke-Expression` runs. In Python it reads every program of a command a runner gives a shell, such as
`os.system` or `shell=True`, and of the script a list such as `["bash", "-c", "<script>"]` runs. A wrapper's program,
or what `&` runs, held in a variable is a program the policy cannot read, and fails the declared-tool policy.

Reach for these substitutes instead:

| Instead of | Use |
|---|---|
| `jq` | Python's `json` module, or PowerShell's `ConvertFrom-Json`. A script parses `gh --json` and `gh api` output with the `json` module, adding `--paginate --slurp` to a paginated `gh api` call, and validates its shape. `gh --jq` is for a one-line command in prose, never a script; validation fails when a skill script passes `--jq`, `-q`, or `--template` to `gh` |
| `rg` | `grep -E` in Bash, Python's `re` module, or the agent's built-in search tool |
| `yq` | JSON instead of YAML; Python's standard library cannot read YAML |
| `curl` or `wget` against the GitHub API | `gh api`, which handles authentication |
| `xmllint` | `read_xml` from skill-core's `xml_reader.py`, which refuses a `DOCTYPE` or `ENTITY` declaration before parsing with Python's `xml.etree.ElementTree` |
| `python` or `python3` in a Markdown example or a grant | `{{PYTHON}}` |

If a skill truly needs another tool, add it to `SKILL_TOOLS` in `deployer/tools.py` and declare it, so that `check` reports it and deployment warns when it is missing.

Declare the external tools a skill runs, beyond Git and Python, which every deployment already needs:

```json
{
    "tools": ["dotnet-format"],
    "optional_tools": ["gh"]
}
```

`tools` lists the tools the skill cannot work without. `optional_tools` lists those it uses only when they are installed, such as dotnet-format reading a pull request's base branch through `gh` before falling back to the remote's default branch; a tool may not be in both. The known tools are listed in `SKILL_TOOLS` in `deployer/tools.py`: currently `gh`, `copilot` (optional for every skill), `dotnet-format`, `dotnet`, and `pdftotext`, which the code-review skills declare optional because a review without it is `INCOMPLETE` rather than failed. To add one, add it there with how to find it and its minimum version, if any. `python deploy.py check` reports each declared tool and which skills use it, as optional when every skill using it declares it optional, and deploying warns when a selected skill requires a tool that is not installed. [`docs/skills.md`](skills.md) lists each skill's own declared tools.

A skill runs what its own scripts and Bash and PowerShell examples run, and what the scripts of its `skill_deps` that it reaches run: a dependency script is reached when the skill's own files import it or name its path (`${CLAUDE_SKILL_DIR}/../<dependency>/scripts/<script>.py`), and so is every dependency script a reached one imports. A tool a reached script runs is the skill's, and the skill declares it too. A dependency's other tools are not the skill's: `flag-review-finding` depends on `code-review-core` but reaches none of its `gh` calls, so it declares no tools. Repository validation fails when a skill's own scripts or examples run a known tool it does not declare, when a dependency script it reaches runs one it does not declare, when it declares a tool that nothing it runs or reaches runs, when it lists under `tools` a tool the catalogue makes optional for every skill, or when it runs a command that is neither standard nor declared.

Declare an atomic selection bundle in `source.json`:

```json
{
  "bundles": {
    "code-review-operations": {
      "members": ["review-prs", "update-pr-dashboard"]
    }
  }
}
```

A selectable skill may belong to at most one bundle. Bundle members are hidden as individual prompts, and `--all` selects the bundle as one user-intent root. The manifest records requested bundles/root skills separately from the expanded concrete installation.

### Opt-in skills and bundles

Mark a skill or bundle opt-in when only some users need it, such as one for a single programming language. Set `"opt_in": true` in the skill's `deploy-meta/<name>.json`, or in the bundle's definition in `source.json`; for a skill inside a bundle, mark the bundle instead. Only menu items can be opt-in, so a hidden dependency cannot be.

`--all` skips opt-in items unless the user already chose them, so an existing installation keeps them. Users add one with `--all --include <name>` or by choosing it from the menu, which labels it `(opt-in)`. An opt-in skill another skill declares in `skill_deps` is installed with that skill, but that does not choose it or its bundle. `python deploy.py check` reports a tool as optional when every root that uses it is an opt-in item that is not chosen; a tool an unchosen opt-in skill runs is still needed when a chosen root pulls that skill in, and its line marks each unchosen opt-in root as `opt-in <name>`.

Every selected public skill also receives a generated thin adapter under `~/.agents/skills/<name>`. The adapter points to the authoritative rendered skill under `~/.claude/skills/<name>` and contains no copied workflow implementation. It also states that `${CLAUDE_SKILL_DIR}` in the skill stands for that directory, because those runtimes do not replace the variable and their own base directory is the adapter's, which has no `scripts/`. It carries the skill's rendered description, which Codex and Copilot match requests against and show in their skill listings and pickers, and its `argument-hint` when it has one, which Copilot shows when the user starts the skill, but not its `allowed-tools`. For a skill with `disable-model-invocation: true`, it keeps the same description and sets each runtime's own explicit-only switch, which keeps the model from starting the skill: `disable-model-invocation: true` for Copilot and `policy.allow_implicit_invocation: false` in `agents/openai.yaml` for Codex. When the skill declares a shared Markdown asset in `shared_deps`, such as `runtime-compatibility.md`, the adapter first tells the runtime to read and apply it. Hidden dependencies do not receive adapters. Adapter ownership, conflicts, retained force backups, removal, and recovery are part of the same deployment transaction and manifest. The deployment report lists an adapter as `<name> (runtime adapter, ...)` only when it needs attention or does something its skill does not; the manifest records adapters under the historical key `wrappers`. The `.agents/skills/` copies of this repository's own skills are shims that `tools/skill_shims.py` writes, not adapters.

Supported derived variables are `HOME` and `SOURCE_ROOT`, the directory containing the `deploy.py` that rendered the skill. The configured variables are `REPOS_ROOT` and `UPDATE_CHANNEL`. Derived values pass an allowlist, as configured ones do: letters of any script, digits, spaces, and `/ : . @ _ ( ) -`. A deployment whose home folder or checkout path has another character stops before any change and names the character.

Every token a skill uses must appear in its `required_vars`, and every entry in `required_vars` must be used. Shared assets may use only derived variables. A configured variable that no skill requires, or a derived variable that no skill or shared asset uses, is rejected by validation, so remove it from `deployer/config.py` together with the last skill that needs it. To add one, declare its character allowlist in `CONFIGURED_VARIABLES` and its prompt in `PROMPTS` in `deployer/config.py`, its fixed set of values in `CHOICE_VARIABLES` when it has one, and its value in `DEFAULTS` when a configuration saved before it existed should take one, then require it from the skill that uses it. A variable that is not a directory needs a default, which a `--canary-home` deployment takes.

## Template values

Use a declared variable as an uppercase token:

```text
{{REPOS_ROOT}}
```

Every token used by a selected skill must be available during deployment. The deployer rejects unresolved tokens after rendering.

Substitution depends on where the token appears. In `.json` and `.toml` files, and in `json` and `toml` fenced blocks in Markdown, the value is escaped as string content and the rendered file must still parse. In Bash, PowerShell, Python, and YAML files, in their fenced blocks, and in a Markdown file's frontmatter, a value containing quote, expansion, or escape characters is rejected rather than substituted. The deployer reads a fence labelled `bash`, `sh`, `shell`, `console`, `zsh`, `ksh`, or `dash` as Bash, `powershell`, `pwsh`, or `ps1` as PowerShell, `python`, `python3`, or `py` as Python, and `yaml` or `yml` as YAML, ignoring the label's case; skill-core's `markdown_sections.FENCE_LANGUAGES` is the table it reads the executable labels from. A token in a fence of an executable language it knows no escaping for, `fish`, `cmd`, `bat`, `batch`, JavaScript (`javascript`, `js`, `node`, `mjs`, `cjs`), or TypeScript (`typescript`, `ts`), stops the deployment whatever its value, with `the deployer knows no escaping for a <context> fence; write the example in a Bash, PowerShell, or Python fence`. Markdown prose, a fence with any other label, and `.txt` files receive the value unchanged. A fence is read as CommonMark reads it: three or more backticks or tildes open it, and only the same character, at least as many times, closes it, so a fence of four backticks can show an example fenced with three. The first word of its info string sets its context. The deployer, the ShellCheck extraction, and every validation rule that reads Markdown fences use the one detector in `deployer/render.py`. `{{PYTHON}}` is the one token of another class: an interpreter, which the deployer derives for the platform from a fixed set, `python` or `python3`, and refuses any other value of. It renders unquoted, as the command word it is, and a skill that uses it declares it in `required_vars` as it would `HOME`. Write it wherever a shipped skill's Markdown Bash or PowerShell, a code span in its prose that runs a command, such as a step's inline command, or a shell grant in its `allowed-tools` starts Python. Validation fails on `python` or `python3` written by hand anywhere in such a fence or grant, but it reads a code span only when the interpreter is the span's first word, followed by its options and what it runs: a span that starts Python after another word, as `cd x && python run.py` does, passes validation and still takes the token. A span that names the interpreter or an option alone, such as `python -c`, runs nothing and stays as written. A script under `scripts/` runs from the checkout in its suite, unrendered, so it names the interpreter itself, as `update.sh` does by the shell it runs in. Keep every other token inside quoted strings in executable content, and add an execution fixture whenever a token appears in Bash or PowerShell: a test in `tests/deployer/` whose name contains `with_spaces`. It carries the token in a string literal, binds a value containing spaces, runs the rendered result with that language's shell (`find_bash`, or `find_pwsh`) through `run_tool`, and asserts the run's output against that value. Validation fails when a token in a skill's Bash or PowerShell sits outside quotes, or when no such test covers that token in that language. A token named only in a comment, a run whose output is never compared with the spaced value, or a test that never runs the result covers nothing.

## Shared assets

Place an owned shared file directly under `skills/` and declare it in `source.json`:

```json
{
  "shared_assets": {
    "runtime-compatibility.md": "owner"
  }
}
```

A shared Markdown asset is runtime guidance for Codex and Copilot. Declare it in the skill's `shared_deps`, but do not mention it in `SKILL.md`: Claude runs the skill directly and would spend a read on it every run, and the generated adapter already points other runtimes to it. Validation rejects a `SKILL.md` that names one, so keep anything Claude also needs, such as a rule about posting to GitHub, in the skill itself.

`shared_assets` must be an object, and each role a string, `owner` or `dependency`; source discovery stops with an error naming the asset otherwise. Use the `dependency` role only when another installed source owns the asset. The deployer verifies the owning source and manifest hash before installing a skill that needs the dependency.

A shared asset's name is one file name of letters, digits, dots, hyphens, underscores, and spaces that does not start with a dot, end in a dot or a space, or name a Windows device such as `con` or `nul.md`. The deployer compares names as the file system does, so on Windows `Guide.md` and `guide.md` name one file: an asset may not share its name, in any case, with a skill or another asset of its source, or with an item another source has deployed. A name ending in `.deploying-bak` or containing `.tmp.` is reserved for the deployer's own files.

The manifest records each installed skill's `shared_deps`, and its agents as `agent_deps` (see [Subagent definitions](#subagent-definitions)), and those records move with the skill during `--migrate-from`. A shared asset is installed only while a selected skill needs it. When no selected skill needs an owned asset, the deployer removes it unless a skill that stays installed still needs it: a locally modified skill that was preserved or skipped, or a skill owned by another source. In that case the asset is kept and reported as `KEEP` until nothing installed needs it.

## Subagent definitions

A Claude Code subagent a skill starts can ship with it. Put its definition in `agents/<name>.md`, a Markdown file whose frontmatter `name` matches the file name, and list it in the skill's `deploy-meta/<name>.json`:

```json
{
  "agent_deps": ["code-review-reviewer"]
}
```

The deployer installs each agent a selected skill declares to `~/.claude/agents/<name>.md`, byte for byte but for the interpreter. Agent files take no other template value, because their frontmatter (`tools`, `model`, `effort`, `hooks`) is executable configuration; `{{PYTHON}}`, a value from a fixed set, renders in them so a hook names Python as the platform does. An agent is owned in the manifest like a skill: an unmanaged file that differs is skipped unless forced, with its backup kept, a locally modified one is preserved, and installs, removals, and recovery share the run's journal. An agent is removed when no selected skill still needs it, unless a locally modified skill that was preserved or skipped was deployed with it: the manifest records each skill's agents as `agent_deps`, their deployed file names, and such an agent is kept and reported as `KEEP` like a shared asset. A skill entry written before `agent_deps` was recorded keeps no agent until a deployment selects the skill and finds it unmodified, which records them. `agents/` holds only `<name>.md` files, and executable logic stays in a skill's `scripts/`. A hook that runs a skill's script cannot use `${CLAUDE_SKILL_DIR}` or `$HOME`: Claude Code runs hooks in Git Bash on Windows, whose `$HOME` follows `HOME` rather than the profile folder the deployer installs into. Have Python find the script instead, in isolated mode so the session's working directory is not on its import path, as `code-review-reviewer` does; `expanduser` reads the profile folder on Windows and `HOME` elsewhere, the home the deployer installs into on each: `{{PYTHON}} -I -B -c "import os, runpy; runpy.run_path(os.path.expanduser('~/.claude/skills/<skill>/scripts/<script>.py'), run_name='__main__')"`. Validation fails when an agent names `$HOME`. Agents are Claude-only: Codex and Copilot follow the skill's instructions with their own subagents, so a skill that names an agent must say what to use when the agent type is not available, as happens in a Claude session started before the deployment.

Manifest version 7 records agents. A version 6 manifest is read and saved as version 7, which an older deployer refuses rather than rewriting a source entry without its agents. A skill entry's `agent_deps` came later within version 7: a deployer that predates it ignores the field and drops it when it rewrites the entry, which loses only the agents a preserved skill would keep, never an owned item.

## Reference documentation

Every selectable skill has a section in [Skills](skills.md), the reference users read to start it:

1. If the skill takes arguments, declare them as `argument-hint` in its frontmatter, using the notation that page explains. A skill that reads `$ARGUMENTS` without one fails validation.
2. Run `python tools/skill_reference.py --write`. It regenerates the summary table and adds the skill's section, whose first block it fills from the frontmatter and metadata.
3. Below that block, write what each argument means, what the skill does with none, and an example invocation. For a skill without arguments, say what it acts on instead.
4. Add a row for the skill, or its bundle, to the "Included skills" table in the README, linking its section here.

Run `python tools/skill_reference.py` to see what is still missing. Validation fails while the reference is stale, a section has no hand-written part, or the README table leaves a skill out, keeps a row for a removed one, or links a section that does not exist.

## Validation

Before adding the skill to a release:

1. Rendering needs no suite of its own: the deployer's rendering suite, `tests/deployer/test_rendering_locking.py`, deploys every shipped skill into a throwaway home, which renders each template and runs the deployer's checks of the rendered Bash, PowerShell, and fences. A new opt-in skill or bundle joins that deployment's `--include`. A token in a skill's Bash or PowerShell also needs the rendered execution fixture [Template values](#template-values) describes, a `*with_spaces*` test under `tests/deployer/`.
2. If the skill contains executable files, include an executable regression suite under its `scripts/` directory. Name each test entry point `test_*`, `test-*`, `*_test`, `*-test`, or `*.test.*` and use `.py`, `.sh`, or `.ps1`. `tests/run_validation.py` discovers and runs these suites automatically, under `tests/` (outside `tests/fixtures/`, whose trees are other repositories a review reads) and each skill's `scripts/`, by name alone. A skill is `skills/<name>` holding `SKILL.md`, as the deployer reads it, or a repository skill under `.claude/skills/`: a file whose name misses the patterns never runs. The deployer leaves every file those names match out of what it deploys, so a suite runs only from the source tree and never where the skill is deployed, and a module a deployed script imports must not be named like one. It runs a Bash or PowerShell suite as a program, and a Python suite as `python -B -m pytest <suite>` from the repository root, where pytest loads the root `conftest.py`; run one the same way, or one test with `python -B -m pytest '<suite>::<Class>::<test>'`. Write a Python suite as unittest test cases, plain pytest test functions, or both, with no `if __name__ == "__main__":` block: validation fails on a Python suite that defines no test, which runs nothing, and on one that holds that block, which would run the suite a second way, without the root `conftest.py`. A table of independent cases is a `@pytest.mark.parametrize` test function, so each case has its own ID, shard, and failure report. A table whose cases build on what a test case's `setUp` makes, or that checks one built artifact several ways, stays a `subTest` loop, since a parametrize list is fixed when the suite is collected, before `setUp` runs. A test function checks through a module-level `CHECK = unittest.TestCase()`, as in `CHECK.assertEqual(expected, actual)`, because the lint rule set's bandit S101 refuses a bare `assert`. The root `conftest.py`'s autouse fixtures isolate every test's git from the shell that started the run, and every class and module fixture's: each runs with a temporary `HOME` and global configuration holding the fixture identity, an empty hooks folder, and git's automatic maintenance off, so nothing git starts outlives the command that started it, no system configuration or attributes, no prompt or editor, and every other `GIT_*` variable but `GIT_EXEC_PATH` and `GIT_BASH` removed, command-scope settings and a hook's `GIT_DIR` among them. A test class or a suite module adds settings, such as a default branch its fixtures name, as a `git_config` mapping of `section.key` to value, and a test reads its home as `self.git_home` on a test case or through the `git_home` fixture. Validation fails on a `conftest.py` below the root, and on a suite that defines, assigns, or parametrizes the name of one of those fixtures, any of which would take the fixture's place.
3. Python under `scripts/` passes the format, lint, and type checks that [Python checks](../CONTRIBUTING.md#python-checks) in the contributing guide describes, with no file or rule exempt; neither the complexity nor the length ceiling takes a `noqa`, and validation fails on one that names C901 or PLR0915, so split a function that exceeds one into helpers of one concern. A script that imports a sibling's scripts, as `skill_deps` allows, declares `skill-core` too and puts both on `sys.path` before the imports they serve: the fixed line that finds skill-core, then skill-core's lookup for each other sibling:

   ```python
   sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "skill-core" / "scripts"))
   from skill_roots import sibling_scripts
   sys.path.insert(0, str(sibling_scripts("<dependency>")))
   ```

   `sibling_scripts` in skill-core's `skill_roots.py` is the one place that knows where a sibling lives: beside skill-core, in the deployed home and the checkout alike. A sibling that is absent ends the script with its name on stderr and exit code 1, before `run_main` runs. `python tools/new_skill.py <name> --skill-dep <dependency>` records the dependency, with `skill-core`, and prints the lines its scripts start with. A suite under `tests/` and a module under `tools/` reach a sibling the same way, after their own skill-core statement. Define any path constant after those imports: ruff's E402 accepts an import after a `sys.path` change but not after an assignment, so the pattern needs no exemption. Validation fails on a `sys.path` entry that names a sibling's `scripts` by a hand-written path, on a `sibling_scripts` call whose name is not one string literal or is `skill-core`, and on a script that looks up a sibling it does not declare in `skill_deps`. mypy runs on each skill's `scripts/` directory from inside it and resolves a `skill_deps` sibling's modules through `mypy_path` in `pyproject.toml`. When a skill first imports a sibling's scripts, add `$MYPY_CONFIG_FILE_DIR/skills/<dependency>/scripts` there; validation fails until the list matches the `sys.path` changes. PowerShell passes `Invoke-ScriptAnalyzer -Severity Warning,Error`: every `.ps1` under `scripts/` and every `powershell`, `pwsh`, or `ps1` fence in the skill's Markdown, as written with its tokens quoted. No rule is suppressed, by `SuppressMessageAttribute` or a settings file; fix the cause. Validation fails on a `SuppressMessageAttribute`, a ShellCheck `disable` directive, and a `PSScriptAnalyzerSettings.psd1` or `.shellcheckrc` file.
4. Validation also holds these rules across the repository. Each lives in a policy module under `tests/validation/`, with a fixture suite beside it, `tests/validation/test_<module>.py`, that shows a violating input failing and a conforming one passing. A new rule goes in its family's module with its fixture test.
   - Keep every directory under `skills/` a skill or a shared asset `source.json` declares. A skill holds `SKILL.md` directly under `skills/<name>`, and no `SKILL.md` sits deeper; the deployer refuses one that does. Anything else is a folder whose files and suites nothing would find. Group skills with a bundle in `source.json`, not a folder.
   - Do not copy a definition or a constant between files. Validation fails when a module-level function or class of four or more lines, docstrings aside and one in a module-level `if`, `try`, or `with` block included, is defined identically in two non-test Python files under `skills/`, `deployer/`, `tools/`, `.claude/skills/`, or `tests/validation/`, and names every copy; two scripts of one skill count, because one can import the other, and so do two policy modules, which share a helper through `tests/validation/validation_support.py`. It fails the same way when two such files assign the same module-level value under any names: a string or bytes literal of twelve or more characters, a pattern `re.compile` builds from a literal, with the same flags, or a tuple, list, or set of literals that together hold twelve or more characters, compared by its members in any order; `__all__` and the `*_ALLOWED` sanction dictionaries are not compared. Import one copy instead, from skill-core when more than one skill needs it. A copy that must stay, such as a deployer function a deployed skill cannot import, is sanctioned beside the code in every module that holds it, as a module-level `DUPLICATION_ALLOWED = {name: reason}` keyed by that module's own name for the copy, whose reason says why the module cannot import the other copy and names, by repository path, every other file that holds it, which validation checks; nothing sanctions a whole module, so a module more than one part of the repository needs moves to skill-core. An entry whose definition or value is no longer copied fails, so the list cannot go stale.
   - Shared code lives in the hidden `skill-core` skill, and everything in the repository imports it from `skills/skill-core/scripts`: skill scripts through `skill_deps`, the deployer through the one `sys.path` statement in `deployer/__init__.py`, which locates the directory from its own file, never through the configured source path, and each `tools/` module through the same statement written above its skill-core imports, because ruff sorts them above the deployer's. Code that a skill and the deployer or `tools/` both need moves there instead of being copied. Validation fails on a module under `deployer/`, `tools/`, or `skills/` outside skill-core that has a skill-core module's name, on a deployer or `tools/` import of a skill-core module with no such statement before it, on a skill-core module that imports from the deployer or `tools/`, on a deployer or skill-core module that imports anything but the standard library, the deployer, and skill-core, and on any other shipped skill script that imports anything but the standard library, skill-core, its own skill's scripts, and those of the skills its `skill_deps` reach, reading an import by `importlib.import_module` or `__import__` as a statement and refusing one whose name is not a string literal.
   - Keep relative Markdown links working. Every relative link in a `.md` file Git does not ignore, outside `.claude/worktrees/`, must resolve to a file or folder, and a `#fragment` to a heading of its target by GitHub's rule: lower-case, backticks and punctuation other than hyphens and underscores removed, each space turned into a hyphen, and a repeated heading numbered `-1`, `-2`. Links inside code, and links with a scheme such as `https:`, are not checked.
   - Import every module in a test. Each non-test Python module under a shipped or repository skill's `scripts/`, `deployer/`, or `tools/`, other than an `__init__.py`, must be imported by at least one `test_*.py`: by an `import` statement, or through `importlib`, by `import_module` or by `spec_from_file_location` from its path. Mentioning it, running it as a program, or naming a `test_<module>.py` after it does not count.
   - Hold repository skills to the shipped standard. A repository skill under `.claude/skills/`, which loads into every session here as a shipped skill does, passes the same policies, applied to that tree by the same functions. Its `scripts/`, if it has one, is found, laid out, formatted, linted, and type-checked as a shipped skill's is, and its suites run with the rest. Its folder, and its shim's, is named by the rules in "Files", and the frontmatter `name` of each is that folder's name. Its Markdown is held to these rules: no fence of more than five lines, no output path left to the agent ("Working files"), scoped twin shell grants ("Granting tools") that cover each repository tool or test it runs, such as `python -B tools/runtime_canary.py`, and a description a runtime adapter accepts that says when to use it ("Description"). Its `.agents/skills` shim is what `python tools/skill_shims.py --write` writes from it: a `SKILL.md` with the same frontmatter and lines pointing at the skill, saying that its commands and paths resolve from the repository root rather than its folder, and that a write under `.agents` needs an approval under Codex's sandbox, which keeps `.agents` read-only, and, for a skill with `disable-model-invocation: true`, the `agents/openai.yaml` that a user-only skill's runtime adapter carries, so Codex never starts it implicitly. Each file is compared whole, and any other file in a shim folder, a folder with no `SKILL.md`, and any other entry under `.agents/skills` fails. A change to one runs `analyze-skill-cost` on it before the pull request, as a shipped skill's does.
   - Leave no workaround in a suite. A test marked skipped or an expected failure lets its suite pass, so validation, in `tests/validation/workarounds.py`, fails on a unittest or pytest skip or expected-failure marker and a `skipTest` call, and on a `TODO`, `FIXME`, or `XXX` comment, in the Python under the format roots outside `tests/fixtures/`. A test that cannot run on a platform is skipped by a unittest `skipIf` or `skipUnless`, or a `pytest.mark.skipif`, whose condition reads the platform alone, or defined in its class or module under an `if` on the platform. Any other skip that must stay is sanctioned beside the code as a module-level `SKIP_ALLOWED = {scope: reason}`, keyed by the function's or class's qualified name; an entry that sanctions nothing fails.
5. Add failure cases for any new parsing, path, or execution behavior.
6. Run the local checks the change reaches; continuous integration runs the full sequence:

   ```powershell
   python -B tests/run_validation.py --changed
   ```

   For a skill's files it selects the skill's own suites, the suites that import or name its scripts, the static checks for the kinds of file changed, the deployer's rendering suite, and the policy checks. "Validation" in `docs/implementing-changes.md` says what it selects and when a full local run is warranted.
7. If debugging a failure, run the affected Bash, ShellCheck, PSScriptAnalyzer, or Python command directly.
8. When the change touches how the skill names its own files, its `allowed-tools`, its runtime adapter, or its agents, run the `runtime-canary` repository skill. `tools/runtime_canary.py` deploys into a throwaway home with `deploy.py --canary-home`, starts Claude Code, Codex, and Copilot CLI there, and reports whether each found the skill and ran its script from the deployed copy. Each run calls a model, so it never runs inside validation; record its result in the pull request.

Files in this skill

  • adding-a-skill.md66 KB
  • codex-support.md12.5 KB
  • copilot-support.md10.4 KB
  • dependency-updates.md21.7 KB
  • dotnet.md3.7 KB
  • implementing-changes.md14.8 KB
  • installation.md26.5 KB
  • parallel-sessions.md26.8 KB
  • recovery.md22.2 KB
  • releasing.md21.7 KB
  • runtime-prompts/review-document/design-review.txt9.2 KB
  • runtime-prompts/review-insights/synthesis.txt10.9 KB
  • runtime-prompts/review-prs/copilot-entrypoint.txt6.3 KB
  • runtime-prompts/review-prs/generic-review.txt8.7 KB
  • skill-evaluations.md10.7 KB
  • skills.md81.8 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.