Skip to content

Permission-Gated Custom Commands

Custom commands pre-approve specific tools through an allowed-tools frontmatter list, so listed tools run without prompting — signaling the expected surface, not blocking unlisted tools.

The Default Exposure Problem

Custom commands in Claude Code inherit the session's full tool permissions. A /review-pr command that only reads files and runs git diff still has implicit access to Write, delete, and arbitrary shell. Fine when you authored it; a problem when sharing with a team or running it in an unfamiliar context.

Claude Code skills documentation describes the allowed-tools frontmatter field as the mechanism for pre-approving specific tools — reducing silent invocations of unintended ones.

Declaring Allowed Tools

Skills (including commands in .claude/commands/) accept YAML frontmatter between --- markers. The allowed-tools field takes a list of tool names Claude may use when the skill is active:

---
name: review-pr
description: Review the current pull request for issues
allowed-tools: Read, Grep, Glob, Bash(git diff *), Bash(git log *)
---

Review the current pull request...

When this command runs, Claude can read files, search with Grep and Glob, and run git diff and git log variants without prompting. Unlisted tools — Write, Edit, arbitrary Bash — are not blocked: they still require explicit user approval, the same as any tool in a session without an allowlist. The field narrows the set of tools that run silently, not the set that can run at all.

The Bash(git diff *) syntax scopes Bash access to commands starting with that prefix. Claude Code's permissions model supports both full tool names (Read) and prefix-scoped tool access using wildcards (Bash(git diff *)).

What to Include in the Allowlist

Design the allowlist around the minimum set of tools the command legitimately needs — this reduces approval prompts for routine tool use and communicates intent to teammates reading the command file:

Command type Typical allowlist
Code review Read, Grep, Glob, Bash(git diff *), Bash(git log *)
Documentation generation Read, Glob, Write
Dependency audit Read, Bash(npm list *), Bash(pip list *)
Safe exploration Read, Grep, Glob

The read-only pattern (Read, Grep, Glob) is a useful baseline for any command that only needs to inspect code. Add Bash access only for specific, named subcommands.

Preventing Automatic Invocation of Sensitive Commands

By default, Claude can invoke any skill automatically when it judges the skill relevant. For commands with side effects — even if their allowed-tools list is conservative — you may want Claude to require explicit invocation. Set disable-model-invocation: true:

---
name: generate-release-notes
description: Generate release notes from git history
disable-model-invocation: true
allowed-tools: Read, Bash(git log *), Bash(git tag *)
---

This removes the command from Claude's automatic context. It only runs when you type /generate-release-notes. Claude Code documentation notes this also removes the skill description from Claude's active context, so the combination of disable-model-invocation and allowed-tools produces the most constrained command mode.

Sharing Commands with a Team

Commands checked into .claude/commands/ (or .claude/skills/<name>/SKILL.md) ship to everyone who clones the repo. The allowed-tools declaration travels with the file, so the team gets safe defaults without per-invocation review — author intent is machine-readable, not just a comment.

Layering with Session-Level Permissions

Command-level allowed-tools operates on top of session-level permissions, not instead of them. Claude Code evaluates permission rules in deny, then ask, then allow order — if a tool is denied at any level, no other level can allow it. The field narrows the set of tools that run without prompting during the command; it cannot grant access to tools blocked by session-level deny rules.

When This Backfires

allowed-tools is a pre-approval mechanism, not a hard restriction. Three failure conditions to account for:

  • Unlisted tools still run with one approval. If a prompt injection or rogue model call attempts Write, the user sees a single approval prompt — the same guard that exists without any allowed-tools declaration. The allowlist does not add a deny layer; it only removes the prompt for listed tools.
  • Allowlists go stale. A command that gains new capabilities (e.g., a /deploy skill that now needs WebFetch to post status) will silently prompt for unlisted tools until the allowlist is updated. Teams relying on "no prompt = expected behavior" will be surprised.
  • False sense of hard enforcement. Operators who assume allowed-tools blocks tools are wrong. For genuine tool blocking, use session-level deny rules in settings.json or a PreToolUse hook — both operate at a lower level than the skill allowlist and cannot be overridden by frontmatter.

Key Takeaways

  • allowed-tools in command frontmatter pre-approves a named subset of tools — they run without prompting during that command's execution.
  • Unlisted tools are not blocked; they require the same user approval as any tool in a session without an allowlist.
  • The Bash(prefix *) syntax scopes bash access to specific subcommands rather than all shell execution.
  • disable-model-invocation: true prevents Claude from triggering a command automatically — use this for any command with side effects, even conservative ones.
  • Commands with declared allowed-tools are safe to commit to version control and share across a team; the pre-approval intent travels with the file.
  • Session-level deny rules take precedence over allowed-tools; the field narrows the no-prompt set but cannot expand session permissions.
Feedback