Skip to content
AI Intermediate Tutorial

Create Custom Agent Skills for Claude Code

Bundle a script and a SKILL.md into a project skill your whole team can run with one slash command.

Rachel Goldstein
Rachel Goldstein
Dev Tools Editor · Sep 1, 2026 · 7 min read
Create Custom Agent Skills for Claude Code

What you'll build

You'll package a bash script and a short instruction file into a project-level Claude Code skill called /preflight. 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.

Prerequisites

  • Claude Code 2.1.233 or later. Tested on 2.1.252 on macOS. Check with claude --version, update with claude update. The validation step needs 2.1.233 or later; everything else works on older 2.1.x builds too.
  • A git repository with a main branch 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.

Skills follow the Agent Skills 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.

1. Create the skill directory

Run this from the root of your repo:

mkdir -p .claude/skills/preflight/scripts

The directory name becomes the command, so preflight gives you /preflight. Names must be lowercase letters, digits, and single hyphens, up to 64 characters, with no leading or trailing hyphen. Skills in .claude/skills/ are project-scoped and travel with the repo. Put the same folder in ~/.claude/skills/ instead if you want it in every project on your machine; if both exist, the personal one wins.

2. Write the script

Save this as .claude/skills/preflight/scripts/preflight.sh. It takes a base branch (default main), lists what changed, and flags three things reviewers hate: new TODO/FIXME markers, files over 1 MB, and WIP or fixup commits.

#!/usr/bin/env bash
# Pre-PR checks. Usage: preflight.sh [base-branch]   (default: main)
set -u
base="${1:-main}"
status=0
branch="$(git rev-parse --abbrev-ref HEAD)"
echo "branch: $branch"
echo "base:   $base"
if ! git rev-parse --verify -q "$base" >/dev/null; then
  echo "ERROR: base branch '$base' not found"; exit 2
fi
echo "commits ahead of $base: $(git rev-list --count "$base..HEAD")"
echo "changed files:"
git diff --name-only "$base...HEAD" | sed 's/^/  /'
todos="$(git diff "$base...HEAD" | grep -E '^\+.*(TODO|FIXME)' || true)"
if [ -n "$todos" ]; then
  echo "PROBLEM: new TODO/FIXME markers in diff:"
  echo "$todos" | sed 's/^/  /'; status=1
fi
big="$(git diff --name-only --diff-filter=A "$base...HEAD" | while read -r f; do
  [ -f "$f" ] && [ "$(wc -c <"$f")" -gt 1000000 ] && echo "$f"; done)"
if [ -n "$big" ]; then
  echo "PROBLEM: files over 1 MB added:"; echo "$big" | sed 's/^/  /'; status=1
fi
wip="$(git log --format='%h %s' "$base..HEAD" | grep -iE '^[0-9a-f]+ (wip|fixup!|squash!)' || true)"
if [ -n "$wip" ]; then
  echo "PROBLEM: WIP/fixup commits to squash:"; echo "$wip" | sed 's/^/  /'; status=1
fi
[ $status -eq 0 ] && echo "RESULT: ready" || echo "RESULT: not ready"
exit $status

Make it executable and run it once by hand, because Claude will run it the same way:

chmod +x .claude/skills/preflight/scripts/preflight.sh
.claude/skills/preflight/scripts/preflight.sh

Exit code 1 means problems were found, 2 means the base branch doesn't exist. That distinction matters in the next step.

3. Write SKILL.md

Save this as .claude/skills/preflight/SKILL.md. The frontmatter has to start on line 1; a blank line before the opening --- makes Claude Code treat the whole file as body text.

---
name: preflight
description: 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".
argument-hint: "[base-branch]"
arguments: [base]
allowed-tools: Bash(${CLAUDE_SKILL_DIR}/scripts/preflight.sh *)
---

## Working tree

!`git status --short || true`

## Instructions

Run exactly this command, with nothing appended, and read its output:

```bash
${CLAUDE_SKILL_DIR}/scripts/preflight.sh $base
```

The script exits 1 when it finds problems; that is expected. Do not check or print the exit code.

Report in this order:
1. One line: branch name and whether it is ready.
2. Each problem the script found, with the exact file or commit it named.
3. Any uncommitted files from the working-tree section above.

Do not fix anything unless the user asks.

What each piece does:

  • description is 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] declares a named argument. /preflight develop expands $base to develop; a bare /preflight expands it to an empty string, and the script's default kicks in. A positional $0 would stay as literal text when no argument is passed, which is why the named form is safer here.
  • allowed-tools pre-approves one exact command for the turn that invokes the skill. ${CLAUDE_SKILL_DIR} is 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 !`git status --short || true` line is dynamic context injection: Claude Code runs it before Claude sees the file and pastes the output in place. The || true matters. 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 ; echo "exit=$?" to inspect the result, and that extra text breaks the allowed-tools prefix match.

argument-hint, arguments, and allowed-tools with ${CLAUDE_SKILL_DIR} are Claude Code extensions. Only name, description, license, compatibility, metadata, and a plain allowed-tools string are part of the base spec.

4. Validate the frontmatter

claude plugin validate .claude/skills

Expected output:

Validating components in: /path/to/your-repo/.claude/skills

✔ Validation passed

Malformed YAML doesn't stop a skill from loading. Claude Code drops all the metadata and keeps the body, so /preflight still works but Claude never triggers it automatically. This command, or starting Claude Code with --debug and reading the parse error, is how you find out.

5. Commit and share

git add .claude/skills/preflight
git commit -m "Add /preflight skill"

Git stores the executable bit, so teammates get a runnable script on clone. Claude Code watches .claude/skills/ for changes, so a session that was already open picks up the new skill without a restart. If the .claude/skills/ directory itself didn't exist when the session started, restart once.

Verify it works

Start claude in the repo and type /preflight. Autocomplete shows the [base-branch] hint. 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 and look for preflight in the list.

For a scriptable check, run it headless:

claude -p "/preflight main"

On a branch with one WIP commit that adds a TODO, the output looks like this:

`feature/preflight` is not ready for a PR against `main` (1 commit ahead, 1 file changed: `add.js`).

Problems the script found:

1. New TODO marker in the diff, in `add.js`: `// TODO handle NaN`
2. WIP commit to squash: `2531955 wip add helper`

Uncommitted files in the working tree:

- `scratch.txt` (untracked)

Nothing was changed.

Wording 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.

Troubleshooting

frontmatter: YAML frontmatter failed to parse: YAML Parse error: Unexpected EOF from claude plugin validate. Usually an unbalanced quote or a bare colon inside description. 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 "..."; in -p mode the run just ends with an empty result and zero turns. An injected !`command` exited non-zero, which aborts the invocation before Claude sees any of it. Append || true to commands that can legitimately fail, or move the check into the bundled script where a non-zero exit is just output.

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 cd ... && or appended ; echo $?, tighten the instruction in SKILL.md as shown above. If you want the whole tool family approved, add a rule like Bash(git *) to .claude/settings.json instead of the skill, since allowed-tools only lasts one turn.

bash: .claude/skills/preflight/scripts/preflight.sh: Permission denied (exit 126). The executable bit is missing. Run chmod +x on the script and commit again; if a teammate hits it after cloning, check that git config core.fileMode isn't false on their machine.

Next steps

Add disable-model-invocation: true to skills with side effects, such as a /deploy or /release skill, so only a human can start them. Add context: fork with agent: Explore to run a read-heavy skill in a subagent that doesn't pollute your main conversation. When SKILL.md grows past a screen, move reference material into reference.md next to it and link to it; the spec recommends keeping SKILL.md under 500 lines. To ship one skill to many repos, wrap the folder in a plugin, and use the official skill-creator plugin (/plugin install skill-creator@claude-plugins-official) to run with-and-without benchmarks before you tune the description. Run /doctor occasionally: it reports how much context your skill listing costs and which skills you never invoke.

Sources & further reading

  1. Extend Claude with skills — code.claude.com
  2. Agent Skills Specification — agentskills.io
  3. Create and distribute a plugin marketplace (claude plugin validate) — code.claude.com
  4. Commands reference — code.claude.com
  5. Advanced setup — code.claude.com
Rachel Goldstein
Written by
Rachel Goldstein · Dev Tools Editor

Rachel 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.

Discussion 1

Join the discussion

Sign in or create an account to comment and vote.

Kat Sorensen @contrarian_kat · 47 minutes ago

neat idea for keeping preflight checks consistent across a team, but curious how this scales when different devs have different setups

Related Reading