# Skills

> Source: https://parallelworks.com/docs/ai/code/skills

# Skills

A skill is a packaged set of instructions that `pw code` loads from disk: a directory containing a `SKILL.md` file with YAML frontmatter and a markdown body. Skills turn a procedure you would otherwise paste every time — a deploy runbook, a review checklist, a debugging routine — into something you can run with `/name`, or that the agent can reach for on its own when a task matches.

`pw code` implements the open [Agent Skills specification](https://agentskills.io/specification), so a `SKILL.md` bundle written for another compatible tool loads unchanged.

## Anatomy of a Skill

```
<skills-dir>/
  deploy/
    SKILL.md          # required: frontmatter + body
    runbook.md        # optional supporting files, referenced by relative path
    scripts/check.sh
```

The **directory name is the skill name**. The `deploy` directory above becomes the `/deploy` slash command and the `name` the agent passes to the `Skill` tool.

A minimal `SKILL.md`:

```markdown
---
name: deploy
description: Deploy the service to an environment and verify health.
argument-hint: <environment>
allowed-tools: Bash(kubectl get:*) Bash(git status:*)
---
Deploy the app to $ARGUMENTS.

1. Run `git status` to confirm a clean tree.
2. Apply the manifests and wait for the rollout.
3. Report the resulting pod status.

See runbook.md in this skill's directory for the full checklist.
```

Everything after the closing `---` is the **body**, the instructions expanded on invocation. Frontmatter is parsed as strict YAML: invalid YAML, duplicate fields, a missing required field, or a field with the wrong type rejects the skill and explains why under `/skills`. There is no permissive fallback parser.

A single `SKILL.md` is capped at 64 KB, and at most 256 skills load per session.

## Frontmatter Fields

These fields come from the Agent Skills specification:

| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `name` | string | yes | Must exactly match the directory name: 1–64 characters of `a-z`, `0-9`, and non-consecutive dashes, not leading or trailing. |
| `description` | string | yes | What the skill does and when to use it, up to 1024 characters. Shown in the `/` palette and advertised to the model. |
| `license` | string | no | License name, or a reference to a bundled license file. |
| `compatibility` | string | no | An environment requirement, up to 500 characters, passed through to the agent on activation. |
| `metadata` | map of string to string | no | Portable extra metadata. Retained, never interpreted as instructions. |
| `allowed-tools` | space-separated string | no | Tool patterns requested for the invocation's turn. See [Tool Grants](#tool-grants). |

`pw code` adds these extensions:

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `argument-hint` | string | — | Shown after the name in the palette, for example `/deploy <environment>`. Documentation only. |
| `arguments` | list or scalar | — | Names for positional arguments, in order. `arguments: [issue, branch]` lets the body use `$issue` and `$branch` instead of `$0` and `$1`. |
| `allowed-tools` | list | — | A convenience equivalent to the space-separated scalar form. |
| `user-invocable` | bool | `true` | `false` hides the skill from the `/` palette; only the agent can call it. |
| `disable-model-invocation` | bool | `false` | `true` makes the skill slash-only; it is hidden from the agent and the `Skill` tool. |

`user-invocable: false` and `disable-model-invocation: true` are opposite restrictions — one hides the skill from you, the other from the agent. Setting both makes the skill uninvocable.

Unknown metadata that cannot change behavior is retained or safely ignored. Fields that *would* change model, tool, isolation, or activation semantics are rejected outright rather than silently honored: `agent`, `allowedTools`, `allowed_tools`, `context`, `disable_model_invocation`, `disallowed-tools`, `disallowedTools`, `effort`, `hooks`, `model`, `paths`, `shell`, `user_invocable`, and `when_to_use`.

## Where Skills Are Loaded From

`pw code` scans these directories in order, and the **first** occurrence of a skill name wins:

1. `~/.agents/skills/` — your personal skills.
2. `<workspace>/.agents/skills/` — skills checked into the project.
3. `.agents/skills/` in each parent directory up to the git repository root, nearest first.

Personal skills come before project skills, so a repository can never silently replace a skill you installed yourself. A shadowed skill is not hidden — `/skills` shows it along with the exact path that won.

The walk-up stops at the directory holding `.git`, so a skill committed at the repo root is found even when you launch `pw code` from a subdirectory, and discovery never reaches past the project boundary. Outside a git repository, only the workspace's own skill directory is scanned.

:::note These are the only locations
Filesystem discovery is limited to the two `.agents/skills` scopes above. Configuration directories, vendor-specific skill roots, and plugin directories are not searched. Put portable skills in an `.agents/skills` directory.
:::

A project skill's directory, its `SKILL.md`, and its bundled resources must all resolve inside the directory that owns the `.agents/skills` root; external or broken links are rejected. A symlink of an entire personal skill directory is supported, but a bundle containing *internal* symlinks cannot be granted execution trust, since the link target could change outside the directory you reviewed.

## Invoking a Skill

### As a Slash Command

Type `/` in a session to open the command palette. User-invocable skills appear alongside built-in and custom commands with their `argument-hint` and description:

```
/deploy staging
```

The body is expanded, trusted tool grants are applied for that turn, and the result is submitted as your prompt.

A typed `/name` resolves in the order **built-in → skill → custom command**. Every built-in name (`clear`, `help`, `compact`, `diff`, `permissions`, and the rest) is reserved, so a skill can never shadow one; a skill named `clear` will never dispatch. Among the remaining names, a skill shadows a same-named [custom command](/docs/ai/code/slash-commands). If that skill sets `user-invocable: false`, `/name` is simply unavailable rather than falling through to the custom command. `/skills` shows the winner, blocked built-in names, and lower-precedence collisions.

Only one skill runs per message. `/lint /test src` is rejected rather than activating both — invoke them in separate messages, or describe what you want and let the agent activate several through the `Skill` tool. A slash-leading path as an *argument* is fine: `/review /src/main.go` is rejected only if the first argument is itself a command name.

### Through the Skill Tool

Model-invocable skills are advertised to the agent as a list of `name: description` pairs. When a task matches, the agent calls the `Skill` tool with a `name` and `args`:

```json
{ "name": "deploy", "args": "staging" }
```

Naming a skill in prose also works, and trailing constraints are passed through verbatim:

```text
Run the /code-review skill but use fewer subagents than normal.
```

Prose activation depends on the agent choosing to make the call; the tool block in the transcript is what makes the activation visible. Starting the message with the slash invocation is the deterministic route, since the client resolves it before the request is sent.

The tool returns the expanded body as a structured block for the agent to follow, and applies trusted tool grants for the current turn. Calling the same skill again with identical instructions returns a short "already active" result instead of re-injecting the whole body. Skills with `disable-model-invocation: true` are excluded from the list and rejected if called.

:::note Descriptions are the trigger
Only the `description`, collapsed to one line, is advertised to the agent — so write descriptions that make the trigger condition obvious. The advertised catalog is capped at 8,000 characters and the tool's name list at 128 entries, taken alphabetically. Skills past the cap are labeled `model (hidden: over catalog limit)` by `/skills`; enabled user-invocable ones still work by slash command, while a model-only skill past the cap is unavailable until you shrink the collection or make it user-invocable.
:::

## Arguments

When a skill is invoked with arguments, the body is templated before expansion:

| Placeholder | Replaced with |
| --- | --- |
| `$ARGUMENTS`, `$ARGS` | The full argument string, byte for byte, after framing whitespace is trimmed. |
| `$0`, `$1`, … `$9` | The Nth argument, **0-based**, so `$0` is the first. Only matched when the digit is not part of a longer number, so `$100` stays prose. |
| `$ARGUMENTS[N]` | The Nth argument by explicit index, for multi-digit indices such as `$ARGUMENTS[10]`. |
| `$name` | The argument at `name`'s position in the `arguments` frontmatter list. |

Arguments are split shell-style, so a quoted value counts as one: `/migrate "search bar" React` gives `$0` = `search bar` and `$1` = `React`. Backslash escapes work outside single quotes. An unterminated quote or trailing escape is rejected before the skill is activated.

An indexed placeholder with no matching argument stays literal; a *declared named* one expands to empty; an undeclared `$name` stays literal.

With this body:

```markdown
Open a PR from $0 into $1 titled "$ARGUMENTS".
```

`/open-pr feature/login main` expands to:

```
Open a PR from feature/login into main titled "feature/login main".
```

With `arguments: [head, base]` in the frontmatter, the same body reads more clearly as `Open a PR from $head into $base`.

Unless the body consumes the whole string through `$ARGUMENTS` or `$ARGS`, the arguments are also appended as a trailing `ARGUMENTS: <args>` line, so a body that reads only `$0` never silently drops the rest of your request.

### Escaping a Literal `$`

Escape with a backslash to keep a `$` literal before a digit, `ARGUMENTS`, or a declared name. The backslash is consumed:

| Body | Arguments | Result |
| --- | --- | --- |
| `costs \$1.00` | `x y` | `costs $1.00` |
| `see \$ARGUMENTS` | `x` | `see $ARGUMENTS` |
| `\$0 vs $0` | `hit` | `$0 vs hit` |

A backslash before a `$` that is not a placeholder is left alone, so `\$HOME` stays `\$HOME`. In general an odd-length backslash run escapes and an even-length run does not, so `\\$0` keeps both backslashes and still expands.

### The Activation Envelope

Every expansion states the skill's directory:

```
Skill "deploy" loaded from "/repo/.agents/skills/deploy". Relative paths in these
instructions resolve against that directory; open referenced files on demand with
ReadFile, using absolute paths under it.
```

That is why supporting files like `runbook.md` can be referenced by relative path in the body — the agent knows where to find them.

## Dynamic Context

A skill body can embed shell placeholders that run **before** the body reaches the agent, so the session starts with live data already inlined instead of having to go fetch it.

- **Inline** — `` !`command` ``. The `!` is recognized only at the start of a line or right after whitespace, so `KEY=!`cmd`` stays literal. Everything up to the next backtick is the command.
- **Fenced block** — a fence opened with ` ```! ` runs its lines together as one script, and the whole block is replaced with the output. A fence that is never closed is left as literal text rather than executed.

```markdown
---
name: working-tree-summary
description: Summarize the working tree and flag risky changes.
allowed-tools: [Bash(git diff:*)]
---
## Current changes

!`git diff HEAD`

## Instructions

Summarize the diff above in a few bullets, then list anything risky.
```

Injection runs on the already-substituted body, so `` !`echo $0` `` sees your arguments. It is a single left-to-right pass and output is inserted verbatim without being re-scanned, so a command cannot emit a placeholder for a later pass to expand. Commands run with `bash -c` in the workspace directory, each bounded by a 30-second timeout and 100 KB each of captured stdout and stderr.

:::warning Placeholders obey the same permission rules as the Bash tool
A placeholder runs only when shell is already allowed without prompting. [Plan mode](/docs/ai/code/permissions) never executes; read-only needs an explicit allow rule; in accept-edits each command must match your allowlist or a trusted skill's `allowed-tools`. Commands that would normally need confirmation, because they are destructive or reach outside the workspace, are skipped rather than bypassing that confirmation. A command that is not pre-approved is replaced with `[shell command skipped: not pre-approved by allowed-tools]`.
:::

This applies to custom slash commands too, which accept the same `allowed-tools` frontmatter. To turn injection off entirely, set `"disableSkillShellExecution": true` in `.agents/settings.json` or `~/.config/agents/settings.json`; every placeholder is then replaced with `[shell command execution disabled by policy]`.

## Tool Grants

A skill's `allowed-tools` are **not** pre-approved at startup. They merge into the permission engine only when a trusted skill is invoked, and they expire when that turn ends. With `allowed-tools: Bash(git status:*)`:

- Before invoking, a `git status` call still prompts.
- During the invocation's turn, it runs without prompting.
- On the next turn it prompts again, unless the skill is invoked again or one of your own allow rules covers it.

Patterns use the same glob form as `--allowedTools` and the settings allowlist, such as `Bash(git status:*)` or `Bash(kubectl get:*)`. In practice only `Bash` patterns matter: file and search tools do not prompt in accept-edits mode, and skill grants are never consulted for MCP tools.

Patterns must be narrowly scoped. A bare `Bash`, a match-everything wildcard like `Bash(*)` or `Bash(**)`, and shell interpreters like `Bash(bash:*)` or `Bash(sh:*)` are rejected, so a skill cannot quietly grant unrestricted shell access. A skill can never widen your active [permission mode](/docs/ai/code/permissions) either — the mode is a ceiling.

`/permissions` separates your own removable rules from skill rules, which are labeled **current turn only**.

## Trust

Project skills are repository-controlled instructions, so they clear two independent gates.

First, the workspace itself. On first interactive launch, `pw code` lists the repository's `SKILL.md` files alongside its settings, agents, and custom commands and asks whether to apply them. See [Workspace Trust](/docs/ai/code/settings#workspace-trust). One-shot `-p` runs never prompt and ignore unapproved repository definitions.

Second, execution trust. Workspace approval makes a skill available to the palette and the agent, but grants no tool authority: its `allowed-tools` and dynamic shell placeholders stay disabled until you run `/skills trust <name>` on that specific skill. Model invocation can never grant either gate.

The same rule follows authorship for custom commands. Commands in `~/.config/agents/commands/` are implicitly trusted; commands in `<workspace>/.agents/commands/` are repository-controlled, so their `allowed-tools` and dynamic shell stay disabled until you trust them.

Trust decisions are stored in your user settings as canonical paths bound to a hash of the complete bundle, including scripts, references, and assets. Editing any bundled file revokes trust until you re-approve it, and project settings can neither populate nor override these decisions. For the same reason, `allowedTools` in workspace settings files is ignored: allow rules come only from your user settings or the `--allowedTools` flag. Only skills you placed in your own directories are implicitly trusted.

## Managing Skills

```text
/skills
/skills reload
/skills disable deploy
/skills enable deploy
/skills trust deploy
/skills untrust deploy
```

`/skills` lists every skill's source scope and exact location, trust state, invocation modes, description, declared `allowed-tools`, dynamic-shell use, and the exact locations of lower-precedence collisions. It also lists custom commands whose grants are trust-gated, and a **Skill problems** section covering unreadable files, oversized `SKILL.md` files, invalid names, `name`/directory mismatches, and malformed YAML.

Disabling is a user-owned setting bound to the winning skill's name, source, and directory. It survives restarts and content edits, does not reveal a shadowed lower-precedence skill, and cannot be overridden by repository settings. Disabling immediately removes the skill from the palette, the advertised catalog, the tool schema, active resource access, the current conversation, and any live subagents. Re-enabling makes it discoverable again but does not resurrect an earlier activation.

:::note State changes need an idle session
`reload`, `disable`, `enable`, `trust`, and `untrust` require the main session and every live subagent to be idle. If a turn is running, the command changes nothing and asks you to retry once the agents finish, so an in-flight request can never start with one definition and finish with another.
:::

## Activation Lifecycle

Active skill instructions survive [compaction](/docs/ai/code/usage-and-costs#context-compaction) up to a 64 KB preservation budget. Past that, the oldest activations are dropped and deactivated, so re-invoking re-injects them. Skill bodies are excluded from the summarizer's input and reattached only through the authenticated active-skill block, so a summary can never turn removed instructions into untracked state. When you resume a compacted session, retained activations come back only if the same enabled skill is still present; per-turn tool grants do not. `/clear` and starting a new session reset every activation.

A session holds at most 32 distinct active skills. A 33rd is rejected with a prompt to run `/clear` or disable an active one, rather than quietly marking an older skill inactive while its instructions remain in the conversation.

An explicit slash activation is transactional through the agent's first accepted response: the expanded prompt, the activation record, and the resource scope all commit together only after that response completes. Cancelling beforehand rolls all of it back, clears turn-only grants, and shows no activation notice. If an accepted response starts tool work and *that* is interrupted, the activation stays recorded and the notice already shown makes it explicit; the grants still expire at the turn boundary. Interactive runs print an `activated skill` notice, one-shot text runs write it to stderr, and `--output json` reports the names in `activated_skills` without contaminating the answer text.

:::warning Instructions are sent to your model provider
Expanded instructions are hidden from the rendered transcript, but they are sent to the configured model provider and stored in the local session file so resumption and compaction can preserve the activation. This includes any output produced by dynamic shell placeholders. Do not put secrets in skill instructions or emit them as dynamic context.
:::

## Examples

### A Slash-Only Helper

`~/.agents/skills/changelog/SKILL.md`:

```markdown
---
name: changelog
description: Draft a changelog entry from the staged diff.
argument-hint: <version>
disable-model-invocation: true
allowed-tools: Bash(git diff --cached:*)
---
Summarize the staged changes into a changelog entry for version $0.
Run `git diff --cached` to see what's staged, then produce markdown bullets
grouped by feat / fix / chore.
```

Run it with `/changelog v7.63.0`. The agent will never invoke it on its own.

### A Skill the Agent Reaches For

`<repo>/.agents/skills/flaky-test/SKILL.md`:

```markdown
---
name: flaky-test
description: Investigate a flaky Go test — run it repeatedly, isolate the race, propose a fix.
argument-hint: <package-or-test>
allowed-tools: Bash(go test:*)
---
Investigate flakiness in $ARGUMENTS.

1. Run the test 20 times with -count and -race to reproduce.
2. If it fails intermittently, inspect shared state and goroutine ordering.
3. Propose the smallest fix and re-run to confirm stability.
```

Because it is model-invocable with a clear description, asking the agent to chase down a flaky test is enough — no slash command needed.

## Troubleshooting

Skills are discovered at startup. After adding, editing, or removing one mid-session, run `/skills reload` to re-scan, which also refreshes the palette, the tool's name list, and the advertised catalog. Repository definitions reload only if that workspace was approved at launch; if you add the *first* repository definition to an unapproved session, restart `pw code` to see the approval prompt.

| Symptom | Likely cause |
| --- | --- |
| Not in the `/` palette | `user-invocable: false`, a built-in's name, a same-named model-only skill shadowing a custom command, an unapproved repository definition, or a higher-precedence skill won. |
| The agent never invokes it | `disable-model-invocation: true`, the skill is disabled or past the catalog cap, or the `description` is too vague to match the task. |
| Not loaded at all | No `SKILL.md`, wrong directory, a non-portable directory name, or a frontmatter `name` that does not match the directory. Run `/skills`. |
| Edits have no effect | Skills load at startup. Run `/skills reload`. |
| A tool still prompts after invoking | The project skill is untrusted, the pattern is missing or too broad, or the turn-scoped grant already expired. Check `/skills` and `/permissions`. |
| Listed under "Skill problems" | Fix the exact YAML, required-field, type, size, name, symlink, or unsupported-field problem shown, then `/skills reload`. |
| `` !`cmd` `` appears literally | The `!` is not at a line start or after whitespace, or `disableSkillShellExecution` is set. |
| `` !`cmd` `` replaced with a "skipped" notice | The project skill is untrusted, the command is not covered by `allowed-tools`, it needs confirmation, or the mode forbids shell. |
| `$0` grabs the wrong argument | Indices are 0-based, and a quoted `"a b"` counts as one argument. |

## Related Documentation

- [Slash Commands](/docs/ai/code/slash-commands): Built-in commands and custom command files
- [Hooks](/docs/ai/code/hooks): Run commands on session and tool lifecycle events
- [Permissions](/docs/ai/code/permissions): Permission modes and allow rules
- [Settings](/docs/ai/code/settings): Settings files and workspace trust
- [Custom Agents](/docs/ai/code/custom-agents): Agents with their own instructions and tools
