Skill.md
Tool Permission System
Every agent that can edit files, run commands, or call external APIs eventually faces the same question on each tool call: auto-allow, ask the user, or deny? Scatter that decision across individual tools and your safety policy turns into an unauditable mess. This skill collapses it into one permission pipeline — every tool call passes through the same pipeline before execution, and it is the single place that decides allow / ask / deny.
Core Architecture
The pipeline order is deliberate, short-circuiting from the hardest block to the most permissive default:
| Layer | Role |
|---|---|
| deny rules | Hard veto, immediate deny |
| ask rules | Forced confirmation, not skippable even in bypass mode |
checkPermissions() | The tool's own tool-specific logic |
| safetyCheck | Dangerous paths (.git/, .claude/, shell configs) prompt, immune to bypass |
| bypass mode | Fast path, immediate allow |
| allow rules | Allowlist, immediate allow |
| default | passthrough → prompt the user |
There are only three decision behaviors: allow, deny, ask. Don't invent new states.
Tool Safety Attributes: Fail-Closed Defaults
The joint between a tool and the permission system is a set of safety attributes on the tool interface, all fail-closed: when the developer declares nothing, the system assumes the most conservative case, and a tool must actively declare "I'm safe" to relax it.
| Attribute | Default | Meaning |
|---|---|---|
isReadOnly | false | Assume it writes |
isDestructive | false | Treat as irreversible until told otherwise |
isConcurrencySafe | false | Serial by default, avoid data races |
checkPermissions | defer to central system | Tool-specific permission logic (pipeline step 1c) |
Use a factory — buildTool({ ...TOOL_DEFAULTS, ...def }) — as the backstop, so a tool that "forgot to declare" only ends up more conservative, never allowed by default. checkPermissions() sits in a sandwich in the pipeline: generic deny/ask run before it (bypass can't skip them), the generic allow allowlist runs after it, so a tool handles only its own specific rules.
Layered Rule Sources
Rules layer by scope, highest priority first: policySettings (enterprise admin, non-overridable) → userSettings → projectSettings → localSettings → cliArg → command → session. The key is that conflicts are settled by behavior order: the pipeline checks deny across all sources first, then ask, and only then allow — so enterprise enforcement always beats a user allowlist.
When to Use It
Use this skill when building or reviewing:
- allow / ask / deny decisions for tool calls
- rule configuration across project / user / enterprise scopes
- a tool-level fail-closed safety interface (
isReadOnly/isDestructive/isConcurrencySafe/checkPermissions) - non-bypassable guardrails for dangerous operations (safetyCheck)
- a hook system's configuration format and lifecycle
- unattended / CI auto-deny plus an AI-classifier circuit breaker
How to Use
- Implement
hasPermission(tool, input, context)that short-circuits in the order above. - Declare fail-closed safety attributes per tool and implement
checkPermissions()for tool-specific rules and dangerous paths. - Configure layered rules and hooks via settings.json; add the
dontAskwrapper for unattended runs.
Boundaries
This skill only owns the design and implementation of the permission decision. It does not own a tool's business logic, the confirmation-dialog UI, user identity authentication, or the prompt engineering of an AI classifier.