{"slug": "create-custom-agent-skills-for-claude-code", "title": "Create Custom Agent Skills for Claude Code", "summary": "Anthropic's Claude Code 2.1.233 or later enables developers to bundle a bash script and a SKILL.md file into a project-level skill, such as the /preflight command, which runs pre-PR checks and allows Claude to interpret the output. The skill, built on the Agent Skills open standard, can be shared across a team via a git repository, and the tutorial provides a script that flags new TODO/FIXME markers, files over 1 MB, and WIP/fixup commits.", "body_md": "# Create Custom Agent Skills for Claude Code\n\nBundle a script and a SKILL.md into a project skill your whole team can run with one slash command.\n\n[Rachel Goldstein](https://sourcefeed.dev/u/rachel_goldstein)\n\n## What you'll build\n\nYou'll package a bash script and a short instruction file into a project-level [Claude Code](https://code.claude.com/docs/en/overview) skill called `/preflight`\n\n. It runs your pre-PR checks against a base branch, and Claude reads the output and tells you what to fix. Commit the folder and everyone on the team gets the same command, with the same script, without pasting instructions into chat.\n\n## Prerequisites\n\n- Claude Code 2.1.233 or later. Tested on 2.1.252 on macOS. Check with\n`claude --version`\n\n, update with`claude update`\n\n. The validation step needs 2.1.233 or later; everything else works on older 2.1.x builds too. - A git repository with a\n`main`\n\nbranch and a feature branch checked out. Any repo works, including a throwaway one. - bash and git on your PATH. On native Windows, install Git for Windows so Claude Code has a Bash tool; without it, Claude Code falls back to PowerShell and the bundled bash script won't run.\n\nSkills follow the [Agent Skills](https://agentskills.io/specification) open standard, so the folder you build here also loads in other tools that implement the spec, minus the Claude Code-only frontmatter fields called out below.\n\n## 1. Create the skill directory\n\nRun this from the root of your repo:\n\n```\nmkdir -p .claude/skills/preflight/scripts\n```\n\nThe directory name becomes the command, so `preflight`\n\ngives you `/preflight`\n\n. Names must be lowercase letters, digits, and single hyphens, up to 64 characters, with no leading or trailing hyphen. Skills in `.claude/skills/`\n\nare project-scoped and travel with the repo. Put the same folder in `~/.claude/skills/`\n\ninstead if you want it in every project on your machine; if both exist, the personal one wins.\n\n## 2. Write the script\n\nSave this as `.claude/skills/preflight/scripts/preflight.sh`\n\n. It takes a base branch (default `main`\n\n), lists what changed, and flags three things reviewers hate: new `TODO`\n\n/`FIXME`\n\nmarkers, files over 1 MB, and WIP or fixup commits.\n\n``` bash\n#!/usr/bin/env bash\n# Pre-PR checks. Usage: preflight.sh [base-branch]   (default: main)\nset -u\nbase=\"${1:-main}\"\nstatus=0\nbranch=\"$(git rev-parse --abbrev-ref HEAD)\"\necho \"branch: $branch\"\necho \"base:   $base\"\nif ! git rev-parse --verify -q \"$base\" >/dev/null; then\n  echo \"ERROR: base branch '$base' not found\"; exit 2\nfi\necho \"commits ahead of $base: $(git rev-list --count \"$base..HEAD\")\"\necho \"changed files:\"\ngit diff --name-only \"$base...HEAD\" | sed 's/^/  /'\ntodos=\"$(git diff \"$base...HEAD\" | grep -E '^\\+.*(TODO|FIXME)' || true)\"\nif [ -n \"$todos\" ]; then\n  echo \"PROBLEM: new TODO/FIXME markers in diff:\"\n  echo \"$todos\" | sed 's/^/  /'; status=1\nfi\nbig=\"$(git diff --name-only --diff-filter=A \"$base...HEAD\" | while read -r f; do\n  [ -f \"$f\" ] && [ \"$(wc -c <\"$f\")\" -gt 1000000 ] && echo \"$f\"; done)\"\nif [ -n \"$big\" ]; then\n  echo \"PROBLEM: files over 1 MB added:\"; echo \"$big\" | sed 's/^/  /'; status=1\nfi\nwip=\"$(git log --format='%h %s' \"$base..HEAD\" | grep -iE '^[0-9a-f]+ (wip|fixup!|squash!)' || true)\"\nif [ -n \"$wip\" ]; then\n  echo \"PROBLEM: WIP/fixup commits to squash:\"; echo \"$wip\" | sed 's/^/  /'; status=1\nfi\n[ $status -eq 0 ] && echo \"RESULT: ready\" || echo \"RESULT: not ready\"\nexit $status\n```\n\nMake it executable and run it once by hand, because Claude will run it the same way:\n\n```\nchmod +x .claude/skills/preflight/scripts/preflight.sh\n.claude/skills/preflight/scripts/preflight.sh\n```\n\nExit code 1 means problems were found, 2 means the base branch doesn't exist. That distinction matters in the next step.\n\n## 3. Write SKILL.md\n\nSave this as `.claude/skills/preflight/SKILL.md`\n\n. The frontmatter has to start on line 1; a blank line before the opening `---`\n\nmakes Claude Code treat the whole file as body text.\n\n```\n---\nname: preflight\ndescription: Pre-PR checklist for this repo. Runs the bundled preflight script against a base branch and reports what must be fixed before opening a pull request. Use when the user asks if a branch is ready for review, wants a PR check, or says \"preflight\".\nargument-hint: \"[base-branch]\"\narguments: [base]\nallowed-tools: Bash(${CLAUDE_SKILL_DIR}/scripts/preflight.sh *)\n---\n\n## Working tree\n\n!`git status --short || true`\n\n## Instructions\n\nRun exactly this command, with nothing appended, and read its output:\n\n``` bash\n${CLAUDE_SKILL_DIR}/scripts/preflight.sh $base\n```\n\nThe script exits 1 when it finds problems; that is expected. Do not check or print the exit code.\n\nReport in this order:\n1. One line: branch name and whether it is ready.\n2. Each problem the script found, with the exact file or commit it named.\n3. Any uncommitted files from the working-tree section above.\n\nDo not fix anything unless the user asks.\n```\n\nWhat each piece does:\n\n`description`\n\nis the only field Claude uses to decide whether to load the skill on its own, so it names the task and the phrases people use to ask for it. Put the main use case first; the skill listing truncates at 1,536 characters.`arguments: [base]`\n\ndeclares a named argument.`/preflight develop`\n\nexpands`$base`\n\nto`develop`\n\n; a bare`/preflight`\n\nexpands it to an empty string, and the script's default kicks in. A positional`$0`\n\nwould stay as literal text when no argument is passed, which is why the named form is safer here.`allowed-tools`\n\npre-approves one exact command for the turn that invokes the skill.`${CLAUDE_SKILL_DIR}`\n\nis substituted in both the rule and the body, so the path matches wherever the repo is cloned and the script runs without a permission prompt.- The\n`!` git status --short || true``\n\nline is dynamic context injection: Claude Code runs it before Claude sees the file and pastes the output in place. The`|| true`\n\nmatters. Any injected command that exits non-zero aborts the whole invocation. - \"Run exactly this command, with nothing appended\" is there because Claude sometimes tacks on\n`; echo \"exit=$?\"`\n\nto inspect the result, and that extra text breaks the`allowed-tools`\n\nprefix match.\n\n`argument-hint`\n\n, `arguments`\n\n, and `allowed-tools`\n\nwith `${CLAUDE_SKILL_DIR}`\n\nare Claude Code extensions. Only `name`\n\n, `description`\n\n, `license`\n\n, `compatibility`\n\n, `metadata`\n\n, and a plain `allowed-tools`\n\nstring are part of the base spec.\n\n## 4. Validate the frontmatter\n\n```\nclaude plugin validate .claude/skills\n```\n\nExpected output:\n\n```\nValidating components in: /path/to/your-repo/.claude/skills\n\n✔ Validation passed\n```\n\nMalformed YAML doesn't stop a skill from loading. Claude Code drops all the metadata and keeps the body, so `/preflight`\n\nstill works but Claude never triggers it automatically. This command, or starting Claude Code with `--debug`\n\nand reading the parse error, is how you find out.\n\n## 5. Commit and share\n\n```\ngit add .claude/skills/preflight\ngit commit -m \"Add /preflight skill\"\n```\n\nGit stores the executable bit, so teammates get a runnable script on clone. Claude Code watches `.claude/skills/`\n\nfor changes, so a session that was already open picks up the new skill without a restart. If the `.claude/skills/`\n\ndirectory itself didn't exist when the session started, restart once.\n\n## Verify it works\n\nStart `claude`\n\nin the repo and type `/preflight`\n\n. Autocomplete shows the `[base-branch]`\n\nhint. You can also ask in plain words, \"is this branch ready for review?\", and Claude should load the skill on its own. To confirm it's registered, run `/skills`\n\nand look for `preflight`\n\nin the list.\n\nFor a scriptable check, run it headless:\n\n```\nclaude -p \"/preflight main\"\n```\n\nOn a branch with one WIP commit that adds a `TODO`\n\n, the output looks like this:\n\n```\n`feature/preflight` is not ready for a PR against `main` (1 commit ahead, 1 file changed: `add.js`).\n\nProblems the script found:\n\n1. New TODO marker in the diff, in `add.js`: `// TODO handle NaN`\n2. WIP commit to squash: `2531955 wip add helper`\n\nUncommitted files in the working tree:\n\n- `scratch.txt` (untracked)\n\nNothing was changed.\n```\n\nWording varies run to run; the problems listed should match the script's output exactly. On a clean branch you get one line saying it's ready.\n\n## Troubleshooting\n\n** frontmatter: YAML frontmatter failed to parse: YAML Parse error: Unexpected EOF** from\n\n`claude plugin validate`\n\n. Usually an unbalanced quote or a bare colon inside `description`\n\n. Wrap the whole value in double quotes and escape inner quotes. The validator's own note says what the runtime does: \"this skill loads with empty metadata (all frontmatter fields silently dropped).\"**The skill returns nothing.** Interactively you'll see `Shell command failed for pattern \"...\"`\n\n; in `-p`\n\nmode the run just ends with an empty result and zero turns. An injected `!` command``\n\nexited non-zero, which aborts the invocation before Claude sees any of it. Append `|| true`\n\nto commands that can legitimately fail, or move the check into the bundled script where a non-zero exit is just output.\n\n**Permission prompt (or a permission_denials entry in --output-format json) even though allowed-tools is set.** The command Claude ran isn't a prefix match for the rule. Check the denied command; if Claude wrapped it in\n\n`cd ... &&`\n\nor appended `; echo $?`\n\n, tighten the instruction in `SKILL.md`\n\nas shown above. If you want the whole tool family approved, add a rule like `Bash(git *)`\n\nto `.claude/settings.json`\n\ninstead of the skill, since `allowed-tools`\n\nonly lasts one turn.** bash: .claude/skills/preflight/scripts/preflight.sh: Permission denied** (exit 126). The executable bit is missing. Run\n\n`chmod +x`\n\non the script and commit again; if a teammate hits it after cloning, check that `git config core.fileMode`\n\nisn't `false`\n\non their machine.## Next steps\n\nAdd `disable-model-invocation: true`\n\nto skills with side effects, such as a `/deploy`\n\nor `/release`\n\nskill, so only a human can start them. Add `context: fork`\n\nwith `agent: Explore`\n\nto run a read-heavy skill in a subagent that doesn't pollute your main conversation. When `SKILL.md`\n\ngrows past a screen, move reference material into `reference.md`\n\nnext to it and link to it; the spec recommends keeping `SKILL.md`\n\nunder 500 lines. To ship one skill to many repos, wrap the folder in a plugin, and use the official `skill-creator`\n\nplugin (`/plugin install skill-creator@claude-plugins-official`\n\n) to run with-and-without benchmarks before you tune the description. Run `/doctor`\n\noccasionally: it reports how much context your skill listing costs and which skills you never invoke.\n\n## Sources & further reading\n\n-\n[Extend Claude with skills](https://code.claude.com/docs/en/skills)— code.claude.com -\n[Agent Skills Specification](https://agentskills.io/specification)— agentskills.io -\n[Create and distribute a plugin marketplace (claude plugin validate)](https://code.claude.com/docs/en/plugin-marketplaces)— code.claude.com -\n[Commands reference](https://code.claude.com/docs/en/commands)— code.claude.com -\n[Advanced setup](https://code.claude.com/docs/en/setup)— code.claude.com\n\n[Rachel Goldstein](https://sourcefeed.dev/u/rachel_goldstein)· Dev Tools Editor\n\nRachel has been embedded in the developer tooling ecosystem for nearly eight years, covering everything from IDE wars and package-manager drama to the quiet rise of AI-assisted coding. She has a soft spot for open-source maintainers and an unhealthy number of terminal emulators installed on a single laptop.\n\n## Discussion 0\n\nNo comments yet\n\nBe the first to weigh in.", "url": "https://wpnews.pro/news/create-custom-agent-skills-for-claude-code", "canonical_source": "https://sourcefeed.dev/a/create-custom-agent-skills-for-claude-code", "published_at": "2026-09-01 17:42:21+00:00", "updated_at": "2026-09-01 17:53:22.969323+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools"], "entities": ["Anthropic", "Claude Code", "Agent Skills", "Rachel Goldstein"], "alternates": {"html": "https://wpnews.pro/news/create-custom-agent-skills-for-claude-code", "markdown": "https://wpnews.pro/news/create-custom-agent-skills-for-claude-code.md", "text": "https://wpnews.pro/news/create-custom-agent-skills-for-claude-code.txt", "jsonld": "https://wpnews.pro/news/create-custom-agent-skills-for-claude-code.jsonld"}}