# Writing rules for SKILL.md

Read this at step 4, or when `scripts/lint-skill.mjs` flags a description, structure, placeholder or portability issue. Client facts were checked on 2026-11-06 against the pages in `sources.yaml`; re-check them at run time.

Contents:

- The description
- The body, section by section
- Wording that matches the failure
- Naming MCP tools and their fallback
- References, size and compaction
- Placeholders and money
- Dates, terms and example data
- Bundled scripts
- Portability across clients
- Lint rules L01 to L19

## The description

The agent decides whether to load a skill from the name and description alone, and clients cut long listings (checked 2026-11-06):

- Claude Code caps each entry, description plus `when_to_use`, at 1,536 characters. When the whole listing passes its budget (1% of the context window), it drops whole descriptions, starting with the skills invoked least.
- Codex shortens descriptions first once the skills list passes 2% of the context window (8,000 characters when the window size is unknown), and omits some skills with a warning when there are many.

So the first sentence carries the job and the words people type, and the description stays well under the limits. This pack keeps "Use when" within the first 600 characters (lint L04); that number is the pack's own heuristic, not a client limit.

Recipe:

1. First sentence: what the skill produces, in the third person, with the job's own nouns (pacing, overspend, budget file).
2. Second sentence: what it never does, if a neighbouring request is a live change ("never changes budgets").
3. "Use when" followed by real request wording, including phrases that never name the topic ("are we going to overspend", "where will spend land"), and the tool names people mention.
4. No angle brackets, no step list, at most 1,024 characters, ideally under 900.

Weak (fictional):

```yaml
description: Helps the marketing team with pacing and budgets.
```

It names no output, gives no triggers, and "budgets" pulls in every budget-change request.

Strong (fictional, the worked example's v2):

```yaml
description: Reviews month-to-date paid media spend against monthly budgets for Google Ads and Meta and flags overspend or underspend against linear budget pacing, from daily spend exports. Prepares a pacing summary, an exception list and draft budget change rows for approval; never changes budgets. Use when asked for a pacing check or pacing review, whether a platform will overspend or underspend this month, how far ahead of or behind budget search or social is, where spend will land, or for a pacing summary before the budget meeting.
```

Add a "never" or "not for" clause only after an observed false trigger, and phrase it as the category, not the failing prompt. Copying the exact words of a failed test prompt into the description overfits the test set.

## The body, section by section

The order matches [../assets/skill-template.md](../assets/skill-template.md):

- Title and two to four lines: what the skill produces, what it never does, where outputs go.
- When to use: the situations, then the near misses with the skill or tool that fits better. Near misses are what stop trigger theft.
- Inputs: the `martech-context/` files to read first, then what to gather, then the stop rule for each missing input, then the line about credentials and personal data.
- Procedure: numbered steps; each decision states its threshold or condition; one default per decision.
- Common failure modes: one line each, symptom then "Detect:" with the check that catches it.
- Worked example: fictional numbers that a reviewer can recompute.
- Acceptance checks: "what to look at. Pass: what a pass looks like".
- Boundaries: what it never does, who approves what, and the hand-off to official tools in read-only mode.
- Live docs: the standard block plus the sources that matter most.

Decision-rule wording:

- Weak: "consider flagging large variances".
- Strong: "flag a platform when month-to-date spend through the last complete day differs from linear pacing by more than 15%, from day 8; before day 8, report only".

## Wording that matches the failure

Pick the form from what the baseline run got wrong:

- It broke a rule: write a prohibition with its reason. "Use only days with `data_complete` true, because the export loads the current day partially."
- It produced the wrong shape: add a template to `assets/` and tell the agent to fill it.
- It left something out: make it a required field in that template ("Cutoff: last complete day").
- It applied a rule in the wrong situation: key the rule to an observable fact ("from day 8", "when the budgets file has a row for the platform").
- Cut nuance words such as "usually", "where appropriate" and "if relevant". Each one either becomes a condition or goes.

## Naming MCP tools and their fallback

Server names differ per install. With several servers connected, an unqualified or renamed tool makes the agent fail to find it or pick a similarly named tool on another server, which may be one that writes. Anthropic's skill authoring guide (checked 2026-11-06) requires fully qualified names.

- Name each tool as `ServerName:tool_name`, using the server names recorded in `martech-context/stack.md` ("Use the AcmeAds:get_campaign_spend tool for month-to-date spend").
- Name the export fallback next to it ("or the daily spend export named in `stack.md`").
- Add the stop rule: "If the named read tool is not in this session, say so and ask for the export. Never use a similar tool from another server."
- Permission rules name the same tool differently in Claude Code (`mcp__acme_ads__get_campaign_spend`); keep both spellings in `stack.md`.

Failure mode: tool substitution. A renamed server leaves the skill's tool missing, and the agent reaches for a look-alike on a write-capable server. Detect: the session tool list against `stack.md`; a run log that shows a tool the skill does not name. Prevent: qualified names plus the stop rule.

## References, size and compaction

- Keep references one level deep: SKILL.md links to `references/x.md`; a reference may name assets but should not send the agent through a chain of references.
- Link each reference with the condition for reading it ("Read when the lint flags L05").
- Start any file longer than 100 lines with a contents list.
- Keep SKILL.md under 500 lines and about 5,000 tokens (estimate: characters divided by 4).
- After compaction, one client re-attaches only the first 5,000 tokens of each invoked skill, within a 25,000-token shared budget filled from the most recently invoked skill, and it does not re-read the file. Put must-not rules near the top. A rule that must hold on every tool call belongs in a hook or a permission rule, not in prose.

Failure mode: rules lost after compaction. The agent follows the skill early in a long session and drifts later. Detect: a long-session test where the agent skips a late rule. Prevent: stay under 5,000 tokens, must-not rules near the top, hooks for rules that must always hold.

## Placeholders and money

- One client substitutes `$ARGUMENTS` (everything typed after the command), `$ARGUMENTS[N]` and `$N` (0-based positions), and named placeholders declared in an `arguments` key. An indexed placeholder with no argument stays literal. If the content has no placeholder at all, the client appends the arguments as "ARGUMENTS: value". To keep a literal dollar sign before a digit, put one backslash before it; a doubled backslash does not escape it.
- Substitution applies to the whole skill body. The docs mention no exception for code blocks, so a shell example that prints the first column with a dollar sign and a 1 receives the first argument instead.
- Deprecated Codex custom prompts used `$1` to `$9`, `$ARGUMENTS`, uppercase named placeholders filled with KEY=value, and `$$` for a literal dollar sign.
- The rule: write money as USD 1,000 and thresholds as "USD 500 a day". Never a dollar sign followed by a digit in SKILL.md or a command file. Lint L13 flags it, inside code blocks too.

## Dates, terms and example data

- Put "checked YYYY-MM-DD" next to any limit, price, menu path or client behaviour, and keep the source URL in the skill's `sources.yaml`, built from [../assets/sources-template.yaml](../assets/sources-template.yaml). A skill that states no vendor fact ships no `sources.yaml` and drops the sentence that names it from Live docs; lint L11 warns when SKILL.md names a `sources.yaml` the folder lacks.
- Avoid relative time ("since the recent update", "last month"). It is wrong the day after it is written.
- Keep a short "old patterns" note for retired methods the agent may meet in old exports (Universal Analytics IDs, the GA4 Configuration tag), so it recognises them without recommending them.
- One term per concept: choose skill, command, client, owner and reviewer and do not rotate synonyms.
- Example data is fictional only: Acme, Example Co, `example.com`, `jane@example.com`, `GTM-XXXXXXX`, `G-XXXXXXXXXX`, `AW-XXXXXXXXX`, Google Ads customer `123-456-7890`. Nothing from client work.

## Bundled scripts

Add a script only for a check the agent would otherwise redo by hand on every run (counts, totals, ID reconciliation, schema checks). Rules, from the agentskills.io scripts guide plus marketing constraints:

- No interactive prompts; `--help` prints usage; arguments, not questions.
- Structured output on request (`--json`), diagnostics on stderr, helpful error messages.
- Meaningful exit codes: 0 pass, 1 findings, 2 bad input.
- Idempotent; a dry-run mode for anything stateful; predictable output size.
- Built-in modules only, no network, read-only except for one named output folder, forward slashes in paths.
- Refer to scripts by their path relative to the skill folder; one client offers a variable for the skill's own folder (see [client-formats.md](client-formats.md)).
- If the runtime is missing, the agent applies the same rules by hand and labels the result "script not run".

Failure mode: network or dependency in a bundled script. The script fails in an offline sandbox or an API container, which has no network and allows no package installs. Detect: lint L18; one run with the network off. Prevent: built-in modules only.

This skill's own scripts follow these rules: `lint-skill.mjs`, `vet-skill.mjs` and `score-triggers.mjs`, with shared helpers in `skill-files.mjs`.

## Portability across clients

- Default to the six spec keys: `name`, `description`, `license`, `compatibility`, `metadata`, `allowed-tools`. Chat-app uploads, the Skills API and the upload packaging script fail with a hard "Unexpected key" error on anything else.
- One client accepts its own extra keys and silently ignores unknown ones, so a misspelt key never warns. Another vendor's skill-creator validator rejects `compatibility` and angle brackets. Cursor documents `name`, `description`, `paths`, `disable-model-invocation`, `icon`, `color` and `metadata`. Codex documents `name` and `description`.
- Keep invocation control out of the frontmatter when the skill must upload anywhere; set it in each client's own mechanism ([commands.md](commands.md)).
- Test on every model the team uses; smaller models need more explicit steps and an example.

Failure mode: client-only keys break uploads. An upload to a chat app or API fails with an unexpected-key error. Detect: lint L05 in the portable profile. Prevent: spec keys only.

The one planned exception: a manual-only skill used in Cursor keeps `disable-model-invocation: true` in its frontmatter ([commands.md](commands.md)). Lint the canonical folder with `--profile team --allow-keys disable-model-invocation`, which passes that key in L05 and names it in the summary line. The copy uploaded to a chat app or the API drops the key and passes the default portable profile.

## Lint rules L01 to L19

`node scripts/lint-skill.mjs <folder> [--profile portable|claude-code|team] [--allow-keys a,b]`. Portable is the default; team adds sections, style and recommended metadata; `--allow-keys` passes the named keys in L05 in any profile.

- L01 frontmatter missing (error): no SKILL.md, or no `---` block at the top.
- L02 frontmatter does not parse (error), or a value is cut at " #" (warn). Common causes: an unquoted value containing ": ", a value starting with `@`, a backtick or `*`, tabs, an unclosed quote, a duplicate key. One client then loads the body with empty metadata, so automatic triggering stops.
- L03 name (error): missing, not lowercase letters, digits and single hyphens, over 64 characters, different from the folder, containing a reserved vendor word, or a reserved folder name.
- L04 description: missing, over 1,024 characters or containing angle brackets (error); no "Use when", "Use when" after character 600 (the pack's heuristic), over 900 characters, or a first or second person opening (warn).
- L05 keys outside the profile: error in portable and team; warn in claude-code, which ignores unknown keys silently. Keys named in `--allow-keys` pass and are listed in the summary line.
- L06 `compatibility` present (warn).
- L07 metadata not a map, or a value that is not a quoted string: numbers, booleans, empty values and unquoted dates (error); recommended keys missing (warn).
- L08 `allowed-tools` present (warn, it only pre-approves); an unscoped shell, a bare star, a whole MCP server or an MCP tool with a write verb (error).
- L09 size: lines over `--max-lines` (error); estimated tokens over `--max-tokens` (warn) and over 110% of it (error).
- L10 sections (team profile): required headings missing or out of order (error); Common failure modes or Worked example missing (warn).
- L11 links: a relative link to a missing file (error), skipping code spans and fenced blocks, so a regex is not read as a link; a SKILL.md link more than one level deep (warn); a reference not linked from SKILL.md, or an asset or script never named (warn); SKILL.md names `sources.yaml` and the folder has none (warn).
- L12 nested SKILL.md anywhere below the top (error).
- L13 a dollar sign before a digit in the body (warn); placeholders found are listed as info.
- L14 injected shell command in the body (warn).
- L15 `hooks` key (warn).
- L16 secret-shaped values and emails outside example.com, example.org and example.net (error); 15 to 19 digit numbers, phone-like numbers and real-looking GTM, GA4, Google Ads and Universal Analytics IDs (warn).
- L17 style (team profile): em dash, emoji or markdown table line (error); filler words such as `powerful`, `seamless` and `robust` (warn).
- L18 scripts: imports outside the standard library and the skill's own folders and `.py` files, network calls, interactive input, a missing `--help`, or a dependency manifest (warn).
- L19 hygiene: `.DS_Store` or `Thumbs.db` (error); files over 256 KiB, binaries, archives, symlinks or a bundled dependency folder (warn).

Regression fixture and expected counts: [worked-example.md](worked-example.md).
