Three layers, one toolkit

Most teams pick up Claude Code, write a CLAUDE.md, and stop there. That is a fine start, but it leaves three more powerful layers of automation untouched. This guide is about authoring those layers and stitching them together into a reusable, shareable kit. We deliberately do not cover building agentic workflows, multi-agent orchestration, or the planning stage — we have separate guides for those, cross-linked below. Here the subject is narrower and practical: the building blocks you author and package.

  • Custom slash commands — Markdown files you and Claude can invoke with /name. Reusable prompts with frontmatter, arguments, embedded shell, and file references.
  • Hooks — deterministic handlers the harness runs on lifecycle events. The layer that guarantees things happen, rather than merely asking the model to do them.
  • Plugins — a manifest that bundles commands, Skills, hooks and optional MCP servers so a whole team installs one thing.

The single most useful mental model in this entire piece is the advisory-versus-deterministic split. CLAUDE.md and slash commands are advisory: they are context the model reads and chooses how to act on. Hooks are deterministic: the harness always runs them on the matching event, no matter what the model is doing. Get that distinction right and you will know which layer to reach for every time.

Pro tip

Before authoring anything, run /doctor in Claude Code. It reports which Skills and commands are loaded, whether descriptions are being truncated, and where your settings files live. It is the fastest way to confirm a new command or hook is actually being picked up.

Prerequisites

  • A working Claude Code install and a project you can experiment in. A throwaway git repo is ideal — hooks can block tool calls, and you want room to break things safely.
  • Comfort editing JSON (settings.json) and Markdown with YAML frontmatter.
  • jq installed if you want hook scripts to parse the JSON payload they receive on stdin. It is not mandatory, but every example here uses it.
  • A clear head on the two scopes. Personal config lives in ~/.claude/ and follows you across every project. Project config lives in .claude/ inside the repo and is shared with anyone who clones it. Decide which you want before you start writing.

Layer 1 — Custom slash commands

A custom slash command is, at its simplest, a Markdown file whose body is a prompt. Drop a file at .claude/commands/review-pr.md and you can type /review-pr to run it. Drop the same file at ~/.claude/commands/review-pr.md and it is available in every project on your machine. The command name comes from the file name, not from anything inside it.

Worth knowing

The official Claude Code documentation now states that custom commands have been merged into Skills: a file at .claude/commands/deploy.md and a Skill at .claude/skills/deploy/SKILL.md both create /deploy and behave the same way. Existing .claude/commands/ files keep working and support the same frontmatter, so there is nothing to migrate. We use the commands/ form for single-file commands because it is the lightest thing that works, and the Skill form when a command needs supporting files.

Frontmatter you can set

The YAML block between --- markers at the top of the file configures the command. The fields that matter most in day-to-day use:

  • description — what the command does and when to use it. Claude reads this to decide whether to invoke the command automatically, so write it for a reader, not for yourself.
  • argument-hint — a hint shown during autocomplete, for example [pr-number] or [filename] [format].
  • allowed-tools — tools Claude may use without asking for permission while the command is active. Accepts a space- or comma-separated list, or a YAML list.
  • model — the model to use while the command runs, or inherit to keep the current one. The override applies for the rest of that turn.
  • disable-model-invocation — set to true so only you can invoke the command with /name and Claude never triggers it on its own. Use this for anything with side effects, such as a deploy or a commit.

Arguments, shell, and file references

Three substitutions turn a static prompt into a flexible command:

  • $ARGUMENTS expands to everything you type after the command name. Positional access is $ARGUMENTS[0], $ARGUMENTS[1], with the shorthands $0, $1 and so on. Wrap multi-word values in quotes to keep them as one argument.
  • The !`...` syntax runs a shell command before the prompt reaches Claude and substitutes the output inline. This is preprocessing, not something Claude executes — the model only ever sees the result. The ! is recognised only at the start of a line or right after whitespace.
  • @path/to/file pulls a file's contents into the prompt as a reference, so Claude reads the current file rather than guessing at its contents.

Subdirectories namespace commands. A file at .claude/commands/git/sync.md still invokes as /sync, but the subdirectory groups related commands tidily on disk.

A working example: /review-pr

Here is a complete, copy-pasteable command that summarises a pull request and reviews it against your house standards. The shell injection pulls live PR data with the GitHub CLI before Claude sees anything, so the review is grounded in the actual diff:

---
description: Review a pull request for correctness and house standards. Use when asked to review a PR or check a diff before merge.
argument-hint: [pr-number]
allowed-tools: Bash(gh *), Read, Grep
model: inherit
disable-model-invocation: true
---

## Pull request context

- PR diff:        !`gh pr diff $ARGUMENTS`
- Changed files:  !`gh pr diff $ARGUMENTS --name-only`
- PR description: !`gh pr view $ARGUMENTS --json title,body -q '.title + "\n\n" + .body'`

## House standards

@docs/engineering-standards.md

## Your task

Review the pull request above. For each issue, give the file, the line, the
problem, and a concrete fix. Group findings as Blocking, Should-fix, and Nit.
End with a one-line verdict: APPROVE, REQUEST CHANGES, or COMMENT.

Run it with /review-pr 482. The gh pr diff 482 output is inlined, the standards file is pulled in via @docs/..., and disable-model-invocation: true means Claude will not decide to run a PR review on its own — you trigger it deliberately.

Command versus Skill — when to use which

A Skill lives at .claude/skills/<name>/SKILL.md and is model-invoked: Claude reads its description and loads it automatically when a task matches. A command is the same idea in a lighter package. The practical rule:

  • Reach for a single-file command when the whole thing fits in one prompt and you mostly invoke it yourself.
  • Reach for a Skill when you want Claude to discover and apply it automatically, or when it needs supporting files — a template to fill in, a reference document, or a script to run. A Skill is a directory, so it can carry those alongside SKILL.md.

A minimal SKILL.md needs only frontmatter and a body. The directory name becomes the invocation name; the description tells Claude when to use it:

---
name: summarize-changes
description: Summarise uncommitted changes and flag anything risky. Use when the user asks what changed, wants a commit message, or asks to review their diff.
allowed-tools: Bash(git diff *)
---

## Current changes

!`git diff HEAD`

## Instructions

Summarise the changes above in two or three bullet points, then list any risks
you notice such as missing error handling, hardcoded values, or tests that need
updating. If the diff is empty, say there are no uncommitted changes.
Watch out

Commands and Skills are advisory. Even a perfectly written /run-tests command is something Claude chooses to invoke and chooses how to act on. If the rule is "tests must pass before this work is considered done", a command alone will not guarantee it. That is the job of a hook — which is exactly where we go next.

Layer 2 — Hooks

Hooks are the deterministic layer. You configure them in settings.json and the Claude Code harness runs them on lifecycle events — every time, no model judgement involved. This is what makes hooks the right tool for anything that must happen.

The lifecycle events

Claude Code fires hooks at well-defined points. The events you will use most:

  • PreToolUse — before a tool call runs. The only place you can block a call.
  • PostToolUse — after a tool call succeeds. The natural home for auto-formatting and linting.
  • UserPromptSubmit — when you submit a prompt, before Claude processes it. Good for injecting context or rejecting an unsafe request.
  • Stop — when Claude finishes responding.
  • SubagentStop — when a subagent finishes.
  • SessionStart — when a session begins or resumes. Useful for seeding context, such as the current branch or open tickets.
  • Notification — when Claude Code sends a notification.
  • PreCompact — before context compaction, so you can persist state that would otherwise be summarised away.

The documented event set is broader than this — it includes session, file-change, and task events too — but these eight are the workhorses for a team automation kit.

The settings.json hook schema

Hooks nest three levels deep: the event name, an array of matcher groups, and within each group an array of handlers. A handler of type: "command" runs a shell command; the matcher field filters which tools the group applies to (an exact name, a |-separated list, a regex, or "*" for all). Here is the shape:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-generated.sh",
            "timeout": 10
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format.sh"
          }
        ]
      }
    ]
  }
}

Two details to internalise. First, ${CLAUDE_PROJECT_DIR} resolves to the repo root, so the same settings file works on a teammate's machine in Bengaluru or Manchester regardless of where they cloned it. Second, the handler can also be type: "http" (POST to an endpoint) or type: "mcp_tool" (call a tool on an MCP server) — but command is what you want for local lint and test gates.

Blocking edits to generated files — the canonical PreToolUse

This is the example every team needs. We have a hard rule on this very project: never edit generated files such as news/index.html directly — fix the generator script instead. A line in CLAUDE.md states the rule, but a hook enforces it. A PreToolUse hook receives the tool name and input on stdin, and can block the call two ways: exit with code 2 and print the reason to stderr, or exit 0 and return JSON setting permissionDecision to "deny". Here is the script the schema above points at:

#!/usr/bin/env bash
# .claude/hooks/block-generated.sh
set -euo pipefail

# Claude Code sends the event payload as JSON on stdin.
payload="$(cat)"
file_path="$(printf '%s' "$payload" | jq -r '.tool_input.file_path // empty')"

# Patterns for files that are generated and must not be hand-edited.
case "$file_path" in
  */index.html | */dist/* | */build/* | *.generated.* )
    cat <<'JSON'
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "This file is generated. Edit the generator script instead of the output."
  }
}
JSON
    exit 0
    ;;
esac

# Not a generated file — stay silent and let the normal permission flow run.
exit 0

When Claude tries to Edit or Write a matching path, the call never executes and the model is told exactly why. The equivalent exit-code form is even shorter — print the reason to stderr and exit 2 — but the JSON form lets you set the precise permissionDecision and is easier to extend later. Both prevent the edit deterministically.

Avoid

Do not rely on a CLAUDE.md sentence like "never edit generated files" to actually stop edits to generated files. In a long session the model can lose that instruction, or simply weigh it against a competing goal and edit anyway. Advisory rules drift; hooks do not. If it genuinely must not happen, encode it as a hook.

Auto-formatting and linting with PostToolUse

A PostToolUse hook runs after a tool call succeeds. It cannot block the call — it has already happened — but it can run your formatter so every file Claude touches comes out clean, and it can feed lint results back to the model. Here is the format.sh the schema points at:

#!/usr/bin/env bash
# .claude/hooks/format.sh
set -euo pipefail

payload="$(cat)"
file="$(printf '%s' "$payload" | jq -r '.tool_input.file_path // empty')"
[ -z "$file" ] && exit 0
[ -f "$file" ] || exit 0

case "$file" in
  *.py)            ruff format "$file" && ruff check --fix "$file" || true ;;
  *.ts|*.tsx|*.js) npx --yes prettier --write "$file" >/dev/null 2>&1 || true ;;
  *.go)            gofmt -w "$file" || true ;;
esac

exit 0

Now formatting is not a thing anyone remembers to do, or a thing you nag the model about — it simply happens, on every write, identically for every engineer on the team. That consistency matters far more across a distributed India and UK team than it does for a solo developer: there is no "it formats differently on my machine" because the hook is the machine.

Pro tip

For a hard test gate, add a Stop hook that runs your test suite when Claude finishes and, on failure, exits non-zero with the failures on stderr so the model sees them and keeps working. Pair it with the PostToolUse formatter and the PreToolUse blocker and you have a CI-grade safety net running locally, before anything reaches your actual CI in GitHub Actions or GitLab.

Exit codes, in one table

The hook contract is small. Learn the three exit codes and you can write any hook:

Exit code Meaning What the harness does
0 Success Parses stdout for JSON. If a decision is present (e.g. permissionDecision), it is applied. No JSON means no decision.
2 Blocking error Ignores stdout. Feeds stderr back to Claude. On blocking events such as PreToolUse, the action is prevented.
other Non-blocking error Execution continues. A hook-error notice and the first stderr line appear in the transcript; full stderr goes to the debug log.

Every article here is written by a Verified Builder. Want your name on the next one?

AI Tech Connect lists AI engineers, founders and researchers across India and the UK — and the people hiring browse it to find them. Adding your profile is free.

Become a Verified Builder →

Layer 3 — Packaging into a shareable plugin

You now have commands, a Skill or two, and a couple of hooks. Right now they live in one person's repo. A plugin turns that scattered set into a single installable unit: one /plugin install and a teammate has the whole kit. This is where a distributed team wins — your colleague in Pune and your colleague in Bristol run the identical commands, hooks, and tool servers, with no copy-paste and no drift.

The plugin directory layout

A plugin is a directory. The manifest sits in a .claude-plugin/ subfolder; every other component lives at the plugin root:

my-team-kit/
├── .claude-plugin/
│   └── plugin.json          # the manifest (only this file goes here)
├── commands/
│   └── review-pr.md
├── skills/
│   └── summarize-changes/
│       └── SKILL.md
├── hooks/
│   └── hooks.json           # same schema as settings.json "hooks"
└── .mcp.json                # optional: bundled MCP servers

The components are auto-discovered from those default folders, so for most kits the manifest can be tiny. If you want a tool server in the bundle — say an internal ticketing or deployment MCP server — declare it in .mcp.json; our companion guide on building your first MCP server with FastMCP covers writing the server itself.

The plugin.json manifest

The manifest is optional — if omitted, Claude Code auto-discovers components and names the plugin after its directory. Include one when you want metadata and a pinned version. The only required field is name. A minimal, real manifest:

{
  "name": "my-team-kit",
  "version": "1.0.0",
  "description": "Shared Claude Code commands and hooks: PR review, auto-format, and a generated-file guard.",
  "author": {
    "name": "Platform Team",
    "email": "platform@example.com"
  },
  "license": "MIT",
  "keywords": ["claude-code", "hooks", "linting", "pr-review"]
}

Set version deliberately. If you pin it, teammates only get updates when you bump the field — good for a stable release cycle. If you omit it, Claude Code falls back to the git commit SHA, so every commit is treated as a new version — better while you are iterating fast. The manifest can also point at custom component paths (commands, agents, hooks, mcpServers), but if you stick to the default folders above you do not need to.

The marketplace.json catalogue

A marketplace is a catalogue that lists one or more plugins and where to find them. It is just a JSON file you host in a git repo. The top-level fields are name, owner, and a plugins array; each plugin entry needs at least a name and a source:

{
  "name": "my-plugins",
  "owner": {
    "name": "Platform Team",
    "email": "platform@example.com"
  },
  "plugins": [
    {
      "name": "my-team-kit",
      "source": "./plugins/my-team-kit",
      "description": "Shared Claude Code commands and hooks for the whole team.",
      "version": "1.0.0"
    }
  ]
}

The source can be a relative path (as above) or an object pointing at a git host, for example { "source": "github", "repo": "your-org/team-kit" } — which is how you would distribute across an organisation rather than a single repo.

The install flow

Once the marketplace file is hosted, teammates run two commands. The first registers your catalogue; the second installs a plugin from it, addressed as plugin-name@marketplace-name:

# Register the marketplace (local path or a git URL)
/plugin marketplace add your-org/team-kit

# Install a plugin from it
/plugin install my-team-kit@my-plugins

That is the whole onboarding story for a new engineer's Claude Code setup: two lines, and they have your PR-review command, your formatter hook, your generated-file guard, and any bundled MCP server. When you push an update to the marketplace, they refresh with /plugin marketplace update. For a fast-moving team this beats a wiki page of "things to copy into your .claude folder" by a wide margin.

From a verified Builder

"We were emailing each other zip files of .claude snippets and half the team was on a stale version of the lint hook. Wrapping it as a plugin with a marketplace ended that overnight — one install command, and everyone in London and Hyderabad is on the same toolkit. The generated-file blocker alone has saved us a handful of broken deploys."

— Anita, Verified Builder · Hyderabad, IN

The decision table: which layer for which job

This is the comparison to keep within reach. The column that matters most is advisory versus deterministic — it tells you whether the layer can guarantee a behaviour or merely encourage it.

Layer Advisory or deterministic? Who triggers it Reach for it when…
CLAUDE.md Advisory Always in context; model decides how to use it You want standing facts and conventions the model should know about the project.
Slash command Advisory You (or Claude, unless disabled) You repeat the same prompt or checklist and want it one keystroke away.
Skill Advisory Mostly Claude, automatically by description You want a reusable procedure Claude applies on its own, possibly with supporting files.
Hook Deterministic The harness, on a lifecycle event Something must happen every time: block an edit, run the formatter, gate on tests.

A quick worked example. "Prefer British spelling in docs" is a convention — put it in CLAUDE.md. "Review this PR against our standards" is a repeatable prompt — make it a /review-pr command. "Summarise my changes when I ask what changed" is a model-discoverable procedure — make it a Skill. "Tests must pass before this is done" and "never edit generated files" are non-negotiables — make them hooks. Same toolkit, four jobs, the right layer for each.

Conclusion and next steps

You have a full .claude automation toolkit: commands as reusable prompts, hooks as deterministic guarantees, and a plugin to ship the whole thing in one command. The discipline that keeps it useful is the advisory-versus-deterministic split — author commands and Skills for the things you want, and reserve hooks for the things you require. Start small: one /review-pr command, one PostToolUse formatter, one PreToolUse blocker. Once those earn their keep, package them and hand your team a single install line.

From here, the natural next steps are about doing more inside Claude Code. If you want to compose these building blocks into multi-step automation, read our guide to dynamic workflows in Claude Code. To run several Claude agents in parallel on a large task, see orchestrating Claude Code subagents in production. And to get the most leverage out of any of it, start with plan-first Claude Code workflows — the planning stage that saves the most debugging downstream. These three are complementary to this guide: this one is about the parts you author; those are about how you put them to work.

Building this kind of tooling is exactly the work worth showing. If you ship developer-experience automation, agents, or infra in India or the UK, put it on a free Verified Builder profile so the people hiring can find you — or browse the Builders already listed.

Primary references, all under the official docs: the Skills and custom commands guide, the hooks reference, the plugins reference, and the plugin marketplaces guide. Field names and event lists in this article are taken from those pages; verify the current set against the docs, since Claude Code ships frequently.