Skip to main content

Security-First Development with Claude Code

How to ensure your Claude Code workflow produces secure code. From security-auditor agent to PreToolUse hooks and automated auditing.

Updated October 21, 2026: Added the 2026 permission-mode defaults (auto mode for interactive sessions since v2.1.283, Manual for claude -p since v2.1.302), the Bash sandbox settings, --restricted, and permissions.blockReadsOutsideWorkingDirectories; clarified that security-auditor is a custom subagent (the built-in check is /security-review); fixed the hook examples to the documented prompt-hook and exit-code contracts; and updated the managed-settings path and models.

AI-assisted development raises an important question: How do you ensure the code Claude writes is secure?

The answer isn’t to avoid AI assistance—it’s to build security into your workflow using Claude Code’s official tools: permission modes and rules, the Bash sandbox, hooks, and the built-in /security-review command, plus a custom security-auditor subagent.

This guide covers how to configure Claude Code for security-conscious development using official features.

Official Security Tools

The security-auditor Agent

The security-auditor is not a built-in agent: it’s a custom subagent you define yourself (Claude Code’s built-in subagents are Explore, Plan, and general-purpose). For an on-demand check with no setup, run the built-in /security-review command, which reviews your branch’s changes for vulnerabilities. To create the subagent, add a file such as:

---
name: security-auditor
description: Reviews code changes for security vulnerabilities. Use for auth, payment, and admin changes.
tools: Read, Glob, Grep
model: sonnet
---

You are a security auditor. Check changes for OWASP Top 10 issues,
hardcoded secrets, injection, XSS, and authentication bypasses, and
report each finding with its severity.

Save it as .claude/agents/security-auditor.md. Claude then delegates to it through the Agent tool (named Task before v2.1.63):

Agent({
  subagent_type: "security-auditor",
  model: "sonnet",  // or "opus" for critical code
  prompt: `
    Audit the authentication module for security vulnerabilities.
    Check for:
    - OWASP Top 10 vulnerabilities
    - Hardcoded secrets
    - SQL injection patterns
    - XSS vulnerabilities
    - Authentication bypass risks
  `
})

When to use security-auditor:

ScenarioModelWhy
Auth/payment changesSonnet/OpusCritical code
New API endpointsSonnetInput validation
File upload handlingSonnetSanitization
External API integrationSonnetData exposure

Hooks for Automatic Security Enforcement

From the official hooks documentation, hooks automatically enforce security policies without manual intervention.

PreToolUse Hook - Block Dangerous Operations

Configure in .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "prompt",
        "prompt": "If the command contains destructive operations (rm -rf, DROP TABLE, git push --force), respond with {\"ok\": false, \"reason\": \"Destructive command: confirm with the user first\"}. Otherwise respond with {\"ok\": true}.",
        "continueOnBlock": true
      }]
    }]
  }
}

A prompt hook can only answer {"ok": true} or {"ok": false, "reason": "..."}. On PreToolUse, ok: false denies the call, and continueOnBlock: true hands the reason back to Claude so it can adjust. To have Claude Code ask you before specific commands instead, add ask rules such as "Bash(git push --force *)" under permissions.ask.

Available hook events:

EventWhenSecurity Use Case
PreToolUseBefore tool executionBlock dangerous commands
PostToolUseAfter tool executionAudit file changes
StopClaude finishes respondingVerify security review
SessionStartSession beginsLoad security context

Stop Hook - Verify Security Review

{
  "hooks": {
    "Stop": [{
      "hooks": [{
        "type": "agent",
        "prompt": "Run git diff --name-only. If files in auth/, payment/, or admin/ changed and the transcript shows no security-auditor review, respond with {\"ok\": false, \"reason\": \"Run the security-auditor subagent on the changed files before finishing\"}. Otherwise respond with {\"ok\": true}. $ARGUMENTS"
      }]
    }]
  }
}

A prompt hook sees only the hook input, so this check uses an agent hook, which can run commands and read the transcript before deciding. Agent hooks are experimental. On Stop, ok: false sends Claude back to work with the reason as its next instruction.

Permission Settings

From the Claude Code documentation, use settings.json for access control:

{
  "permissions": {
    "allow": [
      "Read",
      "Write",
      "Edit",
      "Bash(npm run *)",
      "Bash(git *)"
    ],
    "deny": [
      "Read(.env)",
      "Read(.env.*)",
      "Read(secrets/**)",
      "Read(**/*credentials*)",
      "Bash(*rm -rf*)",
      "Bash(*DROP*)"
    ]
  }
}

Permission pattern syntax:

  • Bash(npm run *) - Allow all npm run commands (Bash(npm run:*) is the equivalent older form)
  • Read(.env) - Deny reading .env files
  • Read(secrets/**) - Deny reading anything in secrets/
  • Bash(*DROP*) - Deny commands containing DROP

Rules are evaluated deny → ask → allow, and deny rules apply in every permission mode, including bypassPermissions. Write file-path rules as Read(...) or Edit(...): Claude Code never checks a Write(path) rule.

Permission Modes and Defaults (2026)

The permission mode decides whether Claude asks before acting, and the defaults changed in 2026:

  • Interactive sessions start in auto mode. Since Claude Code v2.1.283, terminal and VS Code sessions with no permission mode configured start in auto mode, where a classifier model reviews each action instead of prompting you. Explicit ask rules still prompt, and auto mode can be turned off for an organization.
  • Headless runs start in Manual mode. Since v2.1.302, claude -p and Agent SDK runs with no mode set start in Manual mode (config value default), so any call that needs approval is denied unless an allow rule, hook, or --permission-prompt-tool approves it.
  • Protected paths stay guarded. Writes to paths such as .git, .claude, shell startup files, and .mcp.json prompt you in Manual and acceptEdits modes, go to the classifier in auto mode, and are denied in dontAsk; settings allow rules don’t pre-approve them.

For a sensitive repository, pin the starting mode and keep file tools inside the project:

{
  "permissions": {
    "defaultMode": "default",
    "blockReadsOutsideWorkingDirectories": true
  }
}

"default" is Manual mode, which asks before edits, shell commands, and network access. A project’s .claude/settings.json can set any starting mode except auto and bypassPermissions. blockReadsOutsideWorkingDirectories (v2.1.257+) makes Read, Grep, Glob, and LSP refuse paths outside your working directories in every mode, including bypassPermissions; a true in any settings file applies, so a repository can turn it on but can’t turn off yours. Administrators can remove auto mode or bypass mode with permissions.disableAutoMode and permissions.disableBypassPermissionsMode set to "disable" in managed settings.

Sandboxing and Restricted Mode

Permission rules govern Claude’s tools; the Bash sandbox adds operating-system enforcement for the shell commands Claude runs (macOS, Linux, and WSL2). It is off by default: run /sandbox, or set it in settings:

{
  "sandbox": {
    "enabled": true,
    "failIfUnavailable": true,
    "allowUnsandboxedCommands": false,
    "filesystem": {
      "denyRead": ["~/.aws/credentials"]
    },
    "network": {
      "allowedDomains": ["github.com", "*.npmjs.org"]
    }
  }
}

failIfUnavailable makes Claude Code exit instead of running commands unsandboxed when the sandbox can’t start, and allowUnsandboxedCommands: false (strict sandbox mode) stops Claude from retrying a blocked command outside the sandbox. The sandbox covers shell commands only; file tools, hooks, and local MCP servers run outside it and follow permission rules.

For evaluation harnesses or shared machines, start Claude Code with --restricted (or CLAUDE_CODE_RESTRICTED=1, v2.1.248+). It removes the built-in tools that run commands or code and WebFetch unless you name them in --tools, keeps file tools inside the working directories, ignores user, project, and local settings, and refuses bypassPermissions.

The Security Challenge

When Claude writes code, several risks exist:

  • Insecure patterns copied from training data
  • Missing input validation
  • Improper error handling that leaks information
  • Hardcoded credentials or secrets
  • SQL injection, XSS, and other vulnerabilities

The solution: codify your security requirements using agents and hooks so Claude follows them automatically.

Building a Security-First CLAUDE.md

Security Section Template

# Security Requirements

## Non-Negotiables
These rules cannot be overridden for any reason:

1. **No Secrets in Code**
   - Never hardcode credentials, API keys, or tokens
   - Use environment variables for all secrets
   - Reference secrets through secure config management

2. **Input Validation**
   - All user inputs must be validated
   - Use allowlist validation over blocklist
   - Validate on both client and server

3. **Output Encoding**
   - Encode all outputs for the context (HTML, URL, JS)
   - Use framework-provided escaping mechanisms
   - Never concatenate raw user input into responses

4. **Authentication & Authorization**
   - All endpoints must verify authentication
   - Check authorization before every action
   - Use principle of least privilege

5. **Data Protection**
   - Encrypt sensitive data at rest and in transit
   - Minimize data collection and retention
   - Log access to sensitive data

## Security Review Requirements
These types of changes require explicit security review:
- Authentication/authorization logic
- Payment processing
- Personal data handling
- API endpoint creation
- Database queries
- File uploads
- Email sending
- External API calls

Input Validation Standards

# Input Validation Standards

## Validation Library
Use Zod for all runtime validation:

\`\`\`typescript
import { z } from 'zod';

// Define schema
const UserSchema = z.object({
  email: z.string().email(),
  age: z.number().min(0).max(150),
  name: z.string().min(1).max(100),
});

// Validate
const result = UserSchema.safeParse(input);
if (!result.success) {
  throw new ValidationError(result.error);
}
\`\`\`

## Validation Rules
- String inputs: Define max length
- Numeric inputs: Define valid range
- Email: Use proper email validation
- URLs: Validate protocol and domain
- IDs: Validate format (UUID, numeric, etc.)
- Arrays: Validate length and item types
- Objects: Validate required fields

Secure Coding Patterns

# Secure Coding Patterns

## Database Queries
ALWAYS use parameterized queries:

\`\`\`typescript
// NEVER do this
const query = \`SELECT * FROM users WHERE email = '\${email}'\`;

// ALWAYS do this
const user = await db.user.findUnique({
  where: { email }, // Prisma handles parameterization
});
\`\`\`

## Error Handling
Never expose internal details:

\`\`\`typescript
// NEVER do this
catch (error) {
  return res.status(500).json({ error: error.message });
}

// ALWAYS do this
catch (error) {
  console.error('Internal error:', error); // Log full error
  return res.status(500).json({ error: 'An error occurred' }); // Generic message
}
\`\`\`

## Authentication Checks
Always verify auth at the start:

\`\`\`typescript
export async function handler(req, res) {
  // First: Authenticate
  const user = await getAuthenticatedUser(req);
  if (!user) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  // Second: Authorize
  if (!user.canAccessResource(resourceId)) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  // Then: Proceed with logic
  // ...
}
\`\`\`

Agent Delegation for Security

Configure Claude to automatically involve security review for sensitive changes:

# Agent Delegation Rules

## Automatic Security Review
These changes automatically trigger security-auditor agent:

- Files in: auth/, payment/, admin/
- Functions containing: password, token, secret, key, credential
- Operations: user creation, permission changes, data deletion
- External calls: API integrations, webhooks, email

## Security Agent Configuration
When security-auditor runs, it checks for:
- OWASP Top 10 vulnerabilities
- Hardcoded secrets
- Missing authentication
- SQL injection patterns
- XSS vulnerabilities
- Insecure direct object references
- Missing rate limiting
- Insufficient logging

Implementing with the Agent Tool

// Automatic security review for auth changes
Agent({
  subagent_type: "security-auditor",
  model: "sonnet",
  prompt: `
    Review changes to auth/ directory.
    Check for OWASP Top 10 vulnerabilities:
    1. Injection
    2. Broken authentication
    3. Sensitive data exposure
    4. XML external entities
    5. Broken access control
    6. Security misconfiguration
    7. Cross-site scripting
    8. Insecure deserialization
    9. Using components with known vulnerabilities
    10. Insufficient logging

    Report findings with severity levels.
  `
})

Security Workflow Patterns

Pattern 1: Pre-Implementation Review

Before writing security-sensitive code:

Agent({
  subagent_type: "security-auditor",
  model: "sonnet",
  prompt: `
    Planning to implement password reset functionality.
    Review security considerations:
    1. What vulnerabilities should I address?
    2. What OWASP vulnerabilities are relevant?
    3. What's the secure implementation pattern?
    4. What tests should I write?
  `
})

Pattern 2: Implementation with Guard Rails

While implementing with automatic hooks:

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{
        "type": "prompt",
        "prompt": "If tool_input.file_path is in an auth/, payment/, or admin/ directory, respond with {\"ok\": false, \"reason\": \"Security-sensitive file changed: run the security-auditor subagent on it\"}. Otherwise respond with {\"ok\": true}.",
        "continueOnBlock": true
      }]
    }]
  }
}

Pattern 3: Post-Implementation Audit

After implementing:

Agent({
  subagent_type: "security-auditor",
  model: "opus",  // Use Opus for critical security review
  prompt: `
    Comprehensive security audit of password reset feature.

    Check for:
    - Token generation entropy
    - Timing attacks in token comparison
    - Rate limiting bypass
    - User enumeration
    - Session fixation after reset
    - Proper invalidation of old sessions

    Provide findings categorized by severity:
    - Critical
    - High
    - Medium
    - Low
  `
})

Common Vulnerabilities and Prevention

SQL Injection

Vulnerable:

const query = `SELECT * FROM users WHERE id = ${userId}`;

Secure:

const user = await prisma.user.findUnique({ where: { id: userId } });

Hook protection:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{
        "type": "prompt",
        "prompt": "If the new code builds SQL by concatenating strings with SQL keywords (SELECT, INSERT, UPDATE, DELETE), respond with {\"ok\": false, \"reason\": \"Possible SQL injection: use parameterized queries\"}. Otherwise respond with {\"ok\": true}.",
        "continueOnBlock": true
      }]
    }]
  }
}

Cross-Site Scripting (XSS)

Vulnerable:

<div dangerouslySetInnerHTML={{ __html: userContent }} />

Secure:

<div>{userContent}</div> // React auto-escapes

CLAUDE.md rule:

## XSS Prevention
- Never use dangerouslySetInnerHTML without sanitization
- Use DOMPurify for HTML content that must be rendered
- Escape all dynamic content in non-React contexts

Authentication Bypass

Vulnerable:

if (user.role === 'admin') {
  // Allow action
}

Secure:

if (await authService.hasPermission(user.id, 'admin:action')) {
  // Allow action
}

Sensitive Data Exposure

Vulnerable:

return res.json({ user }); // Includes password hash!

Secure:

return res.json({ user: sanitizeUser(user) });

Hooks for Continuous Security

Pre-Commit Security Check

{
  "hooks": {
    "Stop": [{
      "hooks": [{
        "type": "command",
        "command": "npm run security-scan >&2 || exit 2"
      }]
    }]
  }
}

Exit code 2 from a Stop hook sends Claude back to work with the hook’s stderr as the reason. The onFailure: "block" field (v2.1.295+) has no effect on Stop hooks, so the command itself must exit 2 when the scan fails.

Automatic Secret Detection

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{
        "type": "prompt",
        "prompt": "Scan the new content for potential secrets: API keys, passwords, tokens, private keys. If found, respond with {\"ok\": false, \"reason\": \"Possible secret on lines ...\"}. Otherwise respond with {\"ok\": true}.",
        "continueOnBlock": true
      }]
    }]
  }
}

File Access Control

{
  "permissions": {
    "deny": [
      "Read(.env*)",
      "Read(**/secrets/**)",
      "Read(**/*credential*)",
      "Read(**/*.pem)",
      "Read(**/*.key)",
      "Edit(.env*)",
      "Edit(**/secrets/**)"
    ]
  }
}

Security Testing with test-runner

Like security-auditor, test-runner is a custom subagent you define in .claude/agents/.

Automated Security Test Suite

// Launch parallel security tests
Agent({
  subagent_type: "test-runner",
  model: "haiku",
  prompt: "Run all security-related tests in tests/security/"
})

Agent({
  subagent_type: "security-auditor",
  model: "sonnet",
  prompt: "Verify test coverage for authentication edge cases"
})

Security Test Example

describe('Password Reset Security', () => {
  it('rejects invalid tokens', async () => {
    const response = await request(app)
      .post('/api/auth/reset-password')
      .send({ token: 'invalid', password: 'newpassword' });

    expect(response.status).toBe(400);
    expect(response.body).not.toContain('token');
  });

  it('prevents timing attacks on token validation', async () => {
    // Use constant-time comparison
    const times = [];
    for (let i = 0; i < 100; i++) {
      const start = performance.now();
      await validateToken(`token${i}`);
      times.push(performance.now() - start);
    }
    // Variance should be minimal
    expect(standardDeviation(times)).toBeLessThan(5);
  });

  it('rate limits reset requests', async () => {
    const email = 'test@example.com';

    // First 3 requests succeed
    for (let i = 0; i < 3; i++) {
      const response = await request(app)
        .post('/api/auth/forgot-password')
        .send({ email });
      expect(response.status).toBe(200);
    }

    // 4th request is rate limited
    const response = await request(app)
      .post('/api/auth/forgot-password')
      .send({ email });
    expect(response.status).toBe(429);
  });
});

Security Audit Checklist

Before any deployment:

## Pre-Deployment Security Checklist

### Authentication
- [ ] All endpoints require authentication (except public ones)
- [ ] Tokens expire appropriately
- [ ] Password requirements enforced
- [ ] Account lockout after failed attempts

### Authorization
- [ ] Permission checks on all protected actions
- [ ] No privilege escalation paths
- [ ] Admin functions properly protected

### Data Protection
- [ ] Sensitive data encrypted at rest
- [ ] HTTPS enforced
- [ ] No secrets in code or logs
- [ ] PII handling compliant

### Input/Output
- [ ] All inputs validated
- [ ] All outputs properly encoded
- [ ] File uploads validated and sanitized
- [ ] No SQL injection vulnerabilities

### Logging & Monitoring
- [ ] Security events logged
- [ ] Logs don't contain sensitive data
- [ ] Alerting configured for anomalies

Enterprise Security Features

Claude Code supports enterprise security requirements through its settings hierarchy, where managed settings outrank command-line, local, project, and user settings:

Enterprise Layer (Managed Settings)

// /etc/claude-code/managed-settings.json (Linux and WSL; IT managed)
{
  "permissions": {
    "deny": [
      "Bash(*curl*)",
      "Bash(*wget*)",
      "Read(//etc/**)",
      "Edit(//etc/**)"
    ],
    "disableBypassPermissionsMode": "disable"
  },
  "allowedMcpServers": [
    { "serverName": "filesystem" },
    { "serverName": "memory" }
  ]
}

On macOS the file lives at /Library/Application Support/ClaudeCode/managed-settings.json, and on Windows at C:\Program Files\ClaudeCode\managed-settings.json. Use // for absolute paths in Read and Edit rules.

Audit Logging Configuration

## Audit Requirements
Log these events to secure audit system:
- Authentication attempts (success/failure)
- Authorization decisions
- Data access (especially sensitive data)
- Configuration changes
- API calls to external services

Compliance Integration

## Compliance Requirements
- PCI DSS: No card data in logs, encrypt at rest
- GDPR: Data minimization, right to deletion
- HIPAA: PHI access controls and audit trails
- SOC 2: Change management documentation

Model Selection for Security

From the Claude Code model configuration docs:

TaskRecommended ModelWhy
Quick security scanHaiku 5.5Fast pattern matching
Code reviewSonnet 5.5Balanced analysis
Security auditOpus 5.5Thorough review; the docs suggest max effort for hunting vulnerabilities
Critical securityFable 5.1Highest intelligence

Fixing bugs in your own code is routine work for these models, but security-focused requests can hit safeguards. Anthropic’s Opus 5.5 announcement says: “Users will be able to identify and fix bugs in their code as part of the routine software development lifecycle, but most cybersecurity tasks will be re-routed to Opus 4.8.” (source) Requests that a Fable model’s safety classifiers flag, most often in cybersecurity, trigger automatic model fallback, and offensive-security work such as penetration testing triggers it frequently. For that work, Anthropic points organizations to its trusted access programs, such as the Cyber Verification Program.

Quick model switching (v2.0.65+): Press Option+P (macOS) or Alt+P (Windows/Linux) during prompting.

Getting Started

Today:

  1. Add security-auditor delegation rules to CLAUDE.md
  2. Configure permission denials for sensitive files
  3. Add one PreToolUse hook for dangerous commands
  4. Run /security-review on your current branch

This week:

  1. Implement Stop hook for security verification
  2. Add security test suite
  3. Run security-auditor on critical modules

This month:

  1. Deploy pre-commit security hooks
  2. Integrate security scanning in CI/CD
  3. Train team on security-first development

Security is a journey, not a destination. With Claude Code’s agents and hooks, you can automate security best practices.

Sources: Claude Code Documentation, Security, Permission modes, Sandboxing, Hooks Documentation, Claude Code GitHub