---
title: "Hooks, Skills, Permissions — Claude Code Internals — Vlad Arbatov"
description: "Hook events, skill format, permission modes, sandbox, denial tracking"
image: "https://arbatov.dev/share-claude.png"
---

# Hooks, Skills, and Permissions

---

## Hooks System

### Core Files

- `utils/hooks.ts` (46K) — Main execution engine
- `schemas/hooks.ts` — Schema definitions
- `utils/hooks/` — Utilities (config snapshot, prompt/agent/http hooks)

### Hook Events

20+ lifecycle events:
- **SessionStart**, **SessionEnd**
- **PreToolUse**, **PostToolUse**, **PostToolUseFailure**
- **UserPromptSubmit**
- **PermissionRequest**, **PermissionDenied**
- **Setup**
- **SubagentStart**, **SubagentStop**
- **TaskCreated**, **TaskCompleted**
- **FileChanged**, **CwdChanged**

### Hook Command Types

**1. Shell Command** (`type: 'command'`)
```typescript
{
  type: 'command',
  command: string,
  shell: 'bash' | 'powershell',
  timeout?: number,
  statusMessage?: string,
  once?: boolean,
  async?: boolean,
  asyncRewake?: boolean
}
```

**2. Prompt Hook** (`type: 'prompt'`)
- LLM evaluates prompt with `$ARGUMENTS` placeholder
- Configurable model and timeout

**3. Agent Hook** (`type: 'agent'`)
- Verification agent with structured output
- Default timeout: 60s

**4. HTTP Hook** (`type: 'http'`)
- POSTs JSON to URL with custom headers
- Env var interpolation via `allowedEnvVars`

### Configuration Structure

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "echo checking..." }
        ]
      }
    ]
  }
}
```

### Configuration Sources (highest priority first)

1. Policy (managed settings)
2. Flag (`--settings`)
3. Local (`.claude/settings.local.json`)
4. Project (`.claude/settings.json`)
5. User (`~/.claude/settings.json`)

### Trust Requirements

ALL hooks require workspace trust in interactive mode. Trust implicit in SDK mode. Defense-in-depth against untrusted workspace hooks.

### Execution Features

- **Conditional filtering**: `if` field uses permission syntax (e.g., `Bash(git *)`)
- **Timeouts**: Tool hooks = 10 min; SessionEnd = 1.5s
- **Async execution**: `async: true` → background; `asyncRewake: true` → wake model on exit code 2
- **Output capture**: Stdout/stderr captured in TaskOutput

### Hook Responses

```typescript
{
  continue?: boolean,        // default true
  suppressOutput?: boolean,
  decision?: 'approve' | 'block',
  hookSpecificOutput?: {
    permissionDecision?,
    updatedInput?,
    updatedMCPToolOutput?,
    // ...
  }
}
```

---

## Skills System

### Core Files

- `skills/loadSkillsDir.ts` (850+ lines) — Discovery and loading
- `tools/SkillTool/SkillTool.ts` (800+ lines) — Execution
- `tools/SkillTool/prompt.ts` — System prompt integration
- `skills/bundledSkills.ts` — Built-in skills

### Skill Discovery Sources

1. **Bundled** (compiled binary)
2. **Plugin** (from plugins)
3. **User** (`~/.claude/skills/`)
4. **Project** (`.claude/skills/`)
5. **Local** (`.claude/skills.local/`)
6. **Managed** (admin-controlled)

### Skill File Format

Markdown with YAML frontmatter:

```yaml
---
name: my-skill
description: Brief description
when_to_use: Usage guidance
user-invocable: true
allowed-tools: [Bash, Write, Read]
argument-hint: "<file-path>"
model: sonnet
context: fork    # or 'inline' (default)
paths: ["src/**"]
---

Skill content in markdown...
```

### Key Frontmatter Fields

| Field | Purpose |
|-------|---------|
| `name` | Display name |
| `description` | Brief description (auto-extracted if not specified) |
| `user-invocable` | Whether user can invoke directly (default: true) |
| `allowed-tools` | Tool whitelist for this skill |
| `context` | `inline` (default) or `fork` (subagent) |
| `model` | Model override |
| `paths` | Watch paths for FileChanged hooks |
| `hooks` | Skill-specific hooks (same structure as global) |

### Execution Modes

**Inline** (default):
- Same agent context, shared token budget

**Forked** (`context: 'fork'`):
- Sub-agent with separate budget
- Tracks parent-child relationships
- Isolated state

### Content Substitution

- `${CLAUDE_SKILL_DIR}` → Skill base directory
- `${CLAUDE_SESSION_ID}` → Current session
- `$ARG_NAME` → User arguments
- Inline shell execution (file-based skills only)

### Bundled Skills (skills/bundledSkills.ts)

```typescript
type BundledSkillDefinition = {
  name, description, aliases, whenToUse,
  allowedTools, model, context, agent,
  files: Record<string, string>,  // Reference files
  getPromptForCommand(args, context): Promise<ContentBlockParam[]>
}
```

Reference files extracted to `${CLAUDE_TEMP_DIR}/bundled-skills/${skillName}/` on first invocation.

### System Prompt Budget (tools/SkillTool/prompt.ts)

- Budget: **1% of context window** (default 8,000 chars)
- Per-skill description cap: **250 chars**
- Strategy: bundled skills preserve descriptions; non-bundled truncated to fit

---

## Permissions System

### Core Files

- `utils/permissions/permissions.ts` (1200+ lines) — Decision engine
- `utils/permissions/PermissionMode.ts` — Mode definitions
- `utils/permissions/permissionsLoader.ts` — Rule loading
- `utils/permissions/PermissionRule.ts` — Rule types
- `utils/sandbox/sandbox-adapter.ts` — Sandbox integration

### Permission Modes

| Mode | Behavior |
|------|----------|
| `default` | Ask user for each tool |
| `plan` | Pause mode, require approval |
| `acceptEdits` | Auto-accept file edits |
| `bypassPermissions` | Skip all checks |
| `dontAsk` | Deny unknowns, allow known |
| `auto` | Classifier decides (ant-only) |

### Permission Rules

```typescript
type PermissionRule = {
  source: PermissionRuleSource,
  ruleBehavior: 'allow' | 'deny' | 'ask',
  ruleValue: { toolName: string, ruleContent?: string }
}
```

**Examples**: `Bash(git *)`, `Read(*.ts)`, `Write`, `WebFetch(domain:github.com)`, `Edit(/Users/foo/**)`

### Rule Sources Hierarchy (highest priority first)

1. `policySettings` (admin-enforced)
2. `flagSettings` (CLI)
3. `projectSettings` (`.claude/settings.json`)
4. `userSettings` (`~/.claude/settings.json`)
5. `localSettings` (gitignored)
6. `cliArg` (command-line flags)
7. `session` (temporary)

### Decision Flow

```
Permission mode check
  ↓
Deny rules (blanket denies)
  ↓
Allow rules (auto-approve patterns)
  ↓
Tool-specific checkPermissions()
  ↓
Pre-tool hooks
  ↓
Classifier check (if enabled)
  ↓
Ask user or auto-deny
```

### Decision Reasons

```typescript
type PermissionDecisionReason =
  | { type: 'rule'; rule: PermissionRule }
  | { type: 'hook'; hookName: string; reason?: string }
  | { type: 'classifier'; classifier: string; reason: string }
  | { type: 'mode'; mode: PermissionMode }
  | { type: 'sandboxOverride' }
  | { type: 'workingDir'; reason: string }
  | { type: 'other'; reason: string }
```

### Sandbox Integration (utils/sandbox/sandbox-adapter.ts)

**Filesystem:**
- Always allow: `.`, Claude temp directory
- Always deny: settings files (security)
- Read/Write restrictions from permission rules and `sandbox.filesystem.*` settings

**Network:**
- Allow domains: WebFetch rules + `sandbox.network.allowedDomains`
- Deny domains: deny rules + `sandbox.network.blockDomains`

**Path Resolution:**
- `/path` in permission rules: settings-relative
- `/path` in `sandbox.filesystem.*`: absolute
- `~/path`: home directory
- `./path`: settings-relative

### Denial Tracking (utils/permissions/denialTracking.ts)

```typescript
type DenialTrackingState = {
  denialCount: number,
  lastDenialTime: number,
  shouldFallbackToPrompting: boolean
}
```

After 3 denials within 5 minutes → auto-deny further requests. Prevents infinite permission loops.

### Bash-Specific Classification (utils/permissions/bashClassifier.ts)

- Splits compound commands (`;`, `&&`, `||`, `|`)
- Checks each subcommand independently
- Detects output redirections
- Validates directory changes
