August 13, 2025
Claude Development

Building Custom Permission Modes for Claude Code

Claude Code ships with built-in permission modes — restricted, standard, unrestricted — but the reality of running AI in production is messier. You've got interns who shouldn't touch production, contractors who need read-only access to CI/CD systems, and platform teams who need to audit everything. The default modes don't quite cut it.

That's where custom permission modes come in. We're going to build three production-ready permission profiles that combine hooks with allowlists, giving you fine-grained control over what Claude can do without needing five different Claude Code installations.

The hidden insight here is that permission modes aren't binary. You're not just "locked down" or "open." Real governance is nuanced. You trust some people in some contexts but not others. You want to enable people to be productive while preventing disasters. That requires a permission system that's flexible enough to express those nuances.

Building custom modes is about solving a real organizational problem: how do we give teams the access they need without creating security nightmares? An intern learning the codebase has very different permission needs than a platform engineer deploying infrastructure. A contractor should have different audit requirements than a full-time employee. Custom permission modes let you express those differences.

Table of Contents
  1. Why the Built-In Modes Aren't Enough
  2. Understanding the Permission Architecture
  3. Building Custom Modes: The Pattern
  4. Mode 1: Read-Only Exploration Mode
  5. Mode 2: Supervised Mode for Contractors
  6. Mode 3: Platform Mode for Infrastructure Teams
  7. Registering All Modes
  8. Real-World Rollout Considerations
  9. Composing Modes from Hooks
  10. Testing Your Custom Modes
  11. Rolling Out Custom Modes
  12. The Why Behind the Design
  13. Iterating and Refining Your Permission Modes
  14. Monitoring and Auditing Your Modes
  15. The Governance Mindset
  16. Scaling Your Permission System
  17. The Governance Maturity Model
  18. The Long-Term Perspective

Why the Built-In Modes Aren't Enough

The standard modes are designed for the 80% case — most teams are either "give Claude full freedom" or "lock it down completely." But real governance is nuanced.

You need:

  • Exploration mode for new developers learning your codebase without touching anything
  • Supervised mode for contractors where every action gets logged and reviewed
  • Platform mode for infrastructure teams who need broad permissions but with audit trails

Building these isn't about reinventing the permission system. It's about layering hooks on top of the built-in modes and using allowlists to whitelist specific files or domains that would otherwise be blocked.

Think about your actual team structure. Your junior engineers need to read code and run tests, but they shouldn't be modifying critical systems. Your external contractors might need full database access for their specific work, but you need to audit everything they do. Your platform team touches infrastructure that affects the entire company, so they need broad access but with strict boundaries around what they can touch.

The built-in modes don't know about these distinctions. They're generic. Custom modes let you encode your actual organizational policies into code. An intern isn't "restricted" in the sense that they can't do anything useful. They're restricted in the sense that they can explore and learn but can't break things. That's a more useful permission model.

Understanding the Permission Architecture

Before we build custom modes, let's see how Claude Code's permission system actually works under the hood. This architectural understanding is critical because it shows you where and how to layer your custom logic.

The permission system is actually simpler than it seems. You have modes (which define default permissions) and hooks (which enforce additional constraints). A mode says "by default, you can do these things." A hook says "in this specific context, additional rules apply." They stack together. First the mode determines baseline permissions, then hooks apply additional policy on top.

This stacking is what makes custom modes powerful. You don't have to reimplement the entire permission system. You start with a built-in mode as your base — usually "standard" if you want broad permissions with governance, or "restricted" if you want tight control with exceptions. Then you add hooks that enforce your organizational policies. Those hooks layer on top without breaking the base system.

javascript
// This is what a built-in mode looks like (simplified)
const builtInModes = {
  restricted: {
    allowedDomains: [],
    allowedFiles: ["*.md", "*.txt"],
    canExecuteCode: false,
    canModifyFiles: false,
    hooks: ["PRE_ACTION"],
  },
  standard: {
    allowedDomains: ["github.com", "npmjs.com"],
    allowedFiles: ["**/*"],
    canExecuteCode: true,
    canModifyFiles: true,
    hooks: ["PRE_ACTION", "POST_ACTION"],
  },
};

The key insight here is that modes control what's allowed by default. Hooks then apply additional logic on top. A hook can say "yes, you're normally allowed to do this, but in this context, I'm blocking it."

Custom modes stack two things together: a base permission level (usually standard or restricted) plus custom hooks that enforce additional rules. It's composable governance.

Building Custom Modes: The Pattern

Here's the architecture we're going to use:

  1. Define a mode configuration that specifies the base permissions
  2. Write custom hooks that enforce additional constraints
  3. Register the hooks with Claude Code's runtime
  4. Test that the mode behaves as expected

Let's build our three modes step by step.

Each mode is three components working together. The configuration declares what's allowed. The hook enforces it. The tests verify it works. This separation of concerns makes it easy to reason about each piece. You can read the config file and instantly understand the intent. You can read the hook and understand the implementation. You can run the tests and trust the behavior.

This is also why custom modes scale. As you add more modes or update existing ones, you're just adding more hooks to the list. Each hook is independent. You don't have to touch the core permission system. You're composing behavior from discrete, testable pieces.

Mode 1: Read-Only Exploration Mode

This is for developers who are new to your codebase and need to browse without breaking anything. They can read files, run tests, explore structure — but they cannot create, modify, or delete.

Exploration mode solves a real problem: onboarding. When a new engineer joins, they need to understand the codebase. With exploration mode, you can give them Claude Code access that's completely safe. They can explore as much as they want, ask questions, run tests, understand the architecture — all without being able to accidentally break anything. It's a sandbox for learning.

The key design decision here is that exploration mode isn't "locked down and useless." It's "safe and productive." It's not that Claude can't do anything. It's that Claude can do many useful things, but not dangerous ones. That's the right mental model for learning environments.

Think about your actual onboarding experience. You have a new engineer. They need to understand the system. You don't want to give them full commit access while they're learning. But you also don't want to block them from reading code or running tests. Exploration mode is exactly right for this phase. They can safely explore while you're confident nothing will break. After they've spent a week understanding the system, you graduate them to standard mode.

This is also useful for contractors working on read-only tasks. Auditors who need to review code. Security researchers examining your architecture. Any situation where you want to enable deep exploration without write access. Exploration mode is your tool.

javascript
// config/permission-modes.mjs
export const explorationMode = {
  name: "exploration",
  description: "Read-only access for learning the codebase",
  baseMode: "restricted",
 
  allowedDomains: [
    "github.com/myorg/myrepo", // Read docs only
    "npmjs.com", // Can search packages
  ],
 
  allowedActions: [
    "READ_FILE",
    "READ_DIRECTORY",
    "RUN_TESTS",
    "EXECUTE_QUERY",
    "VIEW_LOGS",
  ],
 
  blockedActions: [
    "WRITE_FILE",
    "DELETE_FILE",
    "MODIFY_FILE",
    "EXECUTE_ARBITRARY_CODE",
    "DELETE_DIRECTORY",
  ],
 
  allowedFilePatterns: [
    "src/**/*",
    "tests/**/*",
    "docs/**/*",
    "README.md",
    "package.json",
  ],
 
  blockedFilePatterns: [
    ".env*",
    "secrets/*",
    "credentials/*",
    "*.key",
    "*.pem",
  ],
};

Now we need a hook that enforces these constraints. When Claude tries to take an action, the hook intercepts it:

javascript
// hooks/exploration-hook.mjs
import { explorationMode } from "../config/permission-modes.mjs";
 
export const explorationHook = async (context) => {
  const { action, target, mode } = context;
 
  // Only enforce if we're in exploration mode
  if (mode !== "exploration") {
    return { allowed: true };
  }
 
  // Block any write/delete operations
  if (explorationMode.blockedActions.includes(action)) {
    return {
      allowed: false,
      reason: `${action} is not allowed in exploration mode`,
      suggestion:
        "Switch to a different permission mode if you need write access",
    };
  }
 
  // Check file patterns
  if (action === "READ_FILE" || action === "WRITE_FILE") {
    const isAllowed = explorationMode.allowedFilePatterns.some((pattern) =>
      matchesGlobPattern(target, pattern),
    );
 
    const isBlocked = explorationMode.blockedFilePatterns.some((pattern) =>
      matchesGlobPattern(target, pattern),
    );
 
    if (isBlocked) {
      return {
        allowed: false,
        reason: `File ${target} is in a restricted pattern`,
        suggestion: "Contact your team lead if you need access to this file",
      };
    }
 
    if (!isAllowed) {
      return {
        allowed: false,
        reason: `File ${target} is not in the allowed exploration paths`,
        suggestion: `Allowed paths: ${explorationMode.allowedFilePatterns.join(", ")}`,
      };
    }
  }
 
  return { allowed: true };
};
 
function matchesGlobPattern(filepath, pattern) {
  // Simple glob matching (use 'minimatch' in production)
  const regex = new RegExp(
    "^" +
      pattern
        .replace(/\*\*/g, ".*")
        .replace(/\*/g, "[^/]*")
        .replace(/\?/g, ".") +
      "$",
  );
  return regex.test(filepath);
}

Here's what this hook does:

  1. Checks the current mode — only enforce if we're actually in exploration mode
  2. Blocks dangerous actions — catches write/delete attempts immediately
  3. Pattern-matches files — allows specific paths, blocks sensitive ones
  4. Explains why — gives clear feedback instead of silent failures

The explanation part is critical. When someone tries to do something that's blocked, the hook doesn't just say "no." It explains why and suggests what they should do instead. That's user-friendly governance. It guides people toward what's allowed rather than just frustrating them with denials.

This is the hidden layer teaching: good permission systems are helpful, not just restrictive. They educate users about boundaries while enabling them within those boundaries.

Register this hook in your .claude/hooks.mjs:

javascript
// .claude/hooks.mjs
import { explorationHook } from "../hooks/exploration-hook.mjs";
 
export const hooks = {
  PRE_ACTION: [explorationHook],
};

Expected behavior in exploration mode:

✅ Claude reads src/components/Button.jsx
✅ Claude views docs/CONTRIBUTING.md
✅ Claude lists files in tests/
❌ Claude tries to write to package.json → "WRITE_FILE not allowed in exploration mode"
❌ Claude tries to read .env.local → "File .env.local is in a restricted pattern"

Mode 2: Supervised Mode for Contractors

Now we're building a mode that allows everything but logs everything. Perfect for contractors or external developers who need real access but you need an audit trail.

This is the "trust but verify" mode. You give contractors genuine access to do their work — they need it to be productive. But you maintain visibility into everything they do. It's not paranoia. It's professional practice. When you're bringing external people into your systems, you need to know what they did, when they did it, and be able to trace the impact of their changes.

Supervised mode is also useful internally for tracking risky operations. Your database administrator might do things that are powerful but dangerous. Supervised mode lets them do their work while creating an audit trail that explains why. If something goes wrong, you have a record of what happened and who did it.

The philosophy here is about transparency over restriction. You're not saying "no, you can't do that." You're saying "yes, you can do that, and here's what we're logging about it." This maintains productivity while enabling accountability. Contractors can move fast. Auditors can trace what happened. Everyone's incentives are aligned — the contractor wants to get work done and will be careful because they know they're being logged.

javascript
// config/permission-modes.mjs (continued)
export const supervisedMode = {
  name: "supervised",
  description: "Full access with mandatory audit logging",
  baseMode: "standard",
 
  auditSettings: {
    logAllActions: true,
    logFilePath: "./logs/claude-audit.jsonl",
    logNetworkRequests: true,
    logFileModifications: true,
    captureBeforeAfter: true,
    notificationEmail: "security@myorg.com",
  },
 
  restrictions: {
    // Even in supervised mode, some things are too dangerous
    blockedCommands: [
      "rm -rf",
      "git push --force",
      "DELETE FROM",
      "DROP TABLE",
    ],
 
    // Domains where we log extra carefully
    sensitiveDomainsRequireApproval: [
      "github.com/myorg",
      "aws.amazon.com",
      "production-db.internal",
    ],
  },
 
  // After any of these actions, send a notification
  notificationTriggers: [
    { action: "WRITE_FILE", pattern: "src/**" },
    { action: "EXECUTE_CODE", pattern: ".*" },
    { action: "NETWORK_REQUEST", domain: "*.internal" },
  ],
};

The hook for supervised mode is more sophisticated — it has to log everything without blocking. The design philosophy here is permissive by default but with comprehensive visibility. You're not trying to prevent things. You're trying to understand what happens.

The key to this mode is the audit log. Every action gets recorded with metadata — who did it, when, what they touched, what they executed. Later you can search this log, analyze patterns, or provide evidence if something goes wrong. The audit log is your accountability mechanism.

javascript
// hooks/supervised-hook.mjs
import fs from "fs";
import path from "path";
import { supervisedMode } from "../config/permission-modes.mjs";
 
export const supervisedHook = async (context) => {
  const { action, target, user, timestamp } = context;
 
  if (context.mode !== "supervised") {
    return { allowed: true };
  }
 
  const auditLog = {
    timestamp: new Date().toISOString(),
    user,
    action,
    target,
    correlationId: context.correlationId,
  };
 
  // Check for obviously dangerous commands
  const isDangerous = supervisedMode.restrictions.blockedCommands.some((cmd) =>
    target?.includes(cmd),
  );
 
  if (isDangerous) {
    auditLog.blocked = true;
    auditLog.reason = "Dangerous command pattern";
    await logAudit(auditLog);
 
    return {
      allowed: false,
      reason: `Command pattern not allowed in supervised mode: ${target}`,
      severity: "high",
    };
  }
 
  // Log the action (allowed or not)
  auditLog.allowed = true;
  await logAudit(auditLog);
 
  // Check if this action requires a notification
  const shouldNotify = supervisedMode.notificationTriggers.some(
    (trigger) =>
      trigger.action === action &&
      (trigger.pattern === ".*" || matchesGlobPattern(target, trigger.pattern)),
  );
 
  if (shouldNotify) {
    // Queue notification (non-blocking)
    queueNotification(auditLog);
  }
 
  return { allowed: true };
};
 
async function logAudit(logEntry) {
  const logDir = path.dirname(supervisedMode.auditSettings.logFilePath);
 
  // Create log directory if it doesn't exist
  if (!fs.existsSync(logDir)) {
    fs.mkdirSync(logDir, { recursive: true });
  }
 
  // Append as JSON Lines format (one JSON object per line)
  fs.appendFileSync(
    supervisedMode.auditSettings.logFilePath,
    JSON.stringify(logEntry) + "\n",
  );
}
 
async function queueNotification(logEntry) {
  // In production, send to your notification service
  // For now, we'll just log it
  console.log(`[NOTIFICATION] ${logEntry.action} on ${logEntry.target}`);
}
 
function matchesGlobPattern(filepath, pattern) {
  const regex = new RegExp(
    "^" +
      pattern
        .replace(/\*\*/g, ".*")
        .replace(/\*/g, "[^/]*")
        .replace(/\?/g, ".") +
      "$",
  );
  return regex.test(filepath);
}

Expected behavior in supervised mode:

✅ Claude writes to src/api.js → logged in audit.jsonl
✅ Claude reads config.json → logged in audit.jsonl
✅ Claude runs npm install → logged, notification sent
❌ Claude tries `rm -rf /` → blocked, marked as dangerous in logs

Your audit log ends up looking like this:

json
{"timestamp":"2025-03-17T14:32:18Z","user":"contractor-bob","action":"WRITE_FILE","target":"src/api.js","allowed":true}
{"timestamp":"2025-03-17T14:33:45Z","user":"contractor-bob","action":"EXECUTE_CODE","target":"npm install","allowed":true}
{"timestamp":"2025-03-17T14:35:22Z","user":"contractor-bob","action":"EXECUTE_CODE","target":"rm -rf /","blocked":true,"reason":"Dangerous command pattern"}

Mode 3: Platform Mode for Infrastructure Teams

The platform team needs different rules than application developers. They need to:

  • Touch infrastructure files (terraform/, k8s/, .github/)
  • Modify CI/CD configurations
  • Access production logs
  • But NOT touch application source code

Platform mode encodes organizational structure into permissions. Your application engineers and platform engineers have different concerns and different responsibilities. They shouldn't be stepping on each other's toes. Platform mode creates clean boundaries between who touches what.

This is solving a real organizational scaling problem. In small teams, everyone touches everything. As you grow, you need specialization. Platform engineers focus on infrastructure. Application engineers focus on features. They need different tools and different permission levels. Platform mode lets you express these organizational boundaries in code.

The psychological benefit is also real. When you know exactly what you're responsible for, you can focus. A platform engineer isn't worrying "should I touch this file?" They know their boundaries. An application engineer isn't tempted to tweak Kubernetes because they know it's not in their mode. Clear boundaries reduce cognitive load and mistakes.

javascript
// config/permission-modes.mjs (continued)
export const platformMode = {
  name: "platform",
  description: "Infrastructure-focused permissions for platform teams",
  baseMode: "standard",
 
  allowedFilePatterns: [
    "terraform/**",
    "kubernetes/**",
    ".github/workflows/**",
    "scripts/**",
    "docker/**",
    "ops/**",
    "docs/**",
    ".env.example",
  ],
 
  blockedFilePatterns: [
    "src/**", // No application code
    "app/**",
    "components/**",
    ".env", // No secrets
    ".env.local",
    "secrets/**",
    "credentials/**",
  ],
 
  allowedDomains: [
    "github.com",
    "aws.amazon.com",
    "docker.io",
    "registry.terraform.io",
    "api.github.com",
  ],
 
  blockedDomains: [
    "stripe.com", // Payment systems
    "api.myapp.com", // Don't call the actual app
  ],
};

The hook for platform mode is about strict file boundaries. The design philosophy here is "fail closed but with clear explanations." If you're not sure whether something is allowed, block it and explain why. The developer can request access if they need it.

This is conservative governance. It assumes that crossing boundaries requires explicit justification. An application engineer who tries to modify a Kubernetes deployment is told no, with an explanation of what they can do and who to ask for help. That's preventing accidental scope creep while remaining helpful.

javascript
// hooks/platform-hook.mjs
import { platformMode } from "../config/permission-modes.mjs";
 
export const platformHook = async (context) => {
  const { action, target, mode } = context;
 
  if (mode !== "platform") {
    return { allowed: true };
  }
 
  // File access control
  if (["READ_FILE", "WRITE_FILE", "DELETE_FILE"].includes(action)) {
    // Check blocked patterns first (fail-safe)
    const isBlocked = platformMode.blockedFilePatterns.some((pattern) =>
      matchesGlobPattern(target, pattern),
    );
 
    if (isBlocked) {
      return {
        allowed: false,
        reason: `${target} is outside platform team's scope`,
        suggestion: `Platform team can access: ${platformMode.allowedFilePatterns.join(", ")}`,
      };
    }
 
    // Then check allowed patterns
    const isAllowed = platformMode.allowedFilePatterns.some((pattern) =>
      matchesGlobPattern(target, pattern),
    );
 
    if (!isAllowed) {
      return {
        allowed: false,
        reason: `${target} is not in an allowed platform directory`,
        suggestion: `Platform team can access: ${platformMode.allowedFilePatterns.join(", ")}`,
      };
    }
  }
 
  // Network access control
  if (action === "NETWORK_REQUEST") {
    const domain = extractDomain(target);
 
    const isBlocked = platformMode.blockedDomains.some((d) =>
      domain.includes(d),
    );
 
    if (isBlocked) {
      return {
        allowed: false,
        reason: `Network access to ${domain} is not allowed`,
        severity: "high",
      };
    }
 
    const isAllowed = platformMode.allowedDomains.some((d) =>
      domain.includes(d),
    );
 
    if (!isAllowed) {
      return {
        allowed: false,
        reason: `Network access to ${domain} requires approval`,
        suggestion: "Contact your security team to request access",
      };
    }
  }
 
  return { allowed: true };
};
 
function matchesGlobPattern(filepath, pattern) {
  const regex = new RegExp(
    "^" +
      pattern
        .replace(/\*\*/g, ".*")
        .replace(/\*/g, "[^/]*")
        .replace(/\?/g, ".") +
      "$",
  );
  return regex.test(filepath);
}
 
function extractDomain(url) {
  try {
    return new URL(url).hostname;
  } catch {
    return url;
  }
}

Expected behavior in platform mode:

✅ Claude modifies terraform/main.tf
✅ Claude updates .github/workflows/deploy.yml
✅ Claude reads docs/DEPLOYMENT.md
✅ Claude calls api.github.com for workflow status
❌ Claude tries to read src/components/Button.jsx → "src/* is outside platform team's scope"
❌ Claude calls stripe.com → "stripe.com is not allowed"

Registering All Modes

Now let's wire everything together in a central registration:

javascript
// .claude/hooks.mjs
import { explorationHook } from "../hooks/exploration-hook.mjs";
import { supervisedHook } from "../hooks/supervised-hook.mjs";
import { platformHook } from "../hooks/platform-hook.mjs";
 
export const permissionModes = {
  exploration: explorationHook,
  supervised: supervisedHook,
  platform: platformHook,
};
 
export const hooks = {
  PRE_ACTION: [
    // All hooks run in order; first to deny wins
    (context) => {
      const currentMode = context.mode || "standard";
      const modeHook = permissionModes[currentMode];
 
      if (modeHook) {
        return modeHook(context);
      }
 
      return { allowed: true };
    },
  ],
};
 
export const config = {
  // Tell Claude Code which modes are available
  availableModes: Object.keys(permissionModes),
  defaultMode: "standard",
};

Real-World Rollout Considerations

When you introduce custom permission modes to your team, there are practical considerations beyond just the technical implementation. First, you need clear communication. People need to understand what the modes are, what they mean for their work, and how they differ from the default modes.

Start with a small pilot. Maybe just your new hires use exploration mode first. Let that stabilize. Then expand to contractors using supervised mode. Then roll out platform mode to your infrastructure team. Gradual rollout lets you catch problems before they affect everyone.

Document everything. Write down which mode each type of person should use. Create runbooks for "I need X permission, how do I get it?" Document how to request mode changes or exceptions. Make the governance transparent and understandable.

Monitor the rollout. Check logs. Talk to people. Are people in the right modes? Are there common denials that are actually needed? Are audit logs providing useful information? Use real-world usage to inform adjustments.

Composing Modes from Hooks

As you gain experience with custom modes, you might want to compose more complex policies from simpler hooks. Instead of having one big hook per mode, you could have smaller hooks for specific concerns — one for file access control, one for network access control, one for audit logging.

This composition pattern scales better as you add more modes. Instead of duplicating file access logic across multiple hooks, you write it once and call it from multiple hooks. You compose behavior from reusable pieces.

The Lodash-style functional composition lets you chain restrictions. A hook can call other hooks or helper functions that verify different aspects of the request. This keeps each piece focused and testable.

Testing Your Custom Modes

Here's a test harness to verify your modes work correctly:

javascript
// test/permission-modes.test.mjs
import {
  explorationHook,
  supervisedHook,
  platformHook,
} from "../hooks/index.mjs";
 
const testCases = [
  {
    mode: "exploration",
    action: "READ_FILE",
    target: "src/index.js",
    shouldAllow: true,
    hook: explorationHook,
  },
  {
    mode: "exploration",
    action: "WRITE_FILE",
    target: "src/index.js",
    shouldAllow: false,
    hook: explorationHook,
  },
  {
    mode: "supervised",
    action: "WRITE_FILE",
    target: "src/api.js",
    shouldAllow: true,
    hook: supervisedHook,
  },
  {
    mode: "platform",
    action: "READ_FILE",
    target: "terraform/main.tf",
    shouldAllow: true,
    hook: platformHook,
  },
  {
    mode: "platform",
    action: "WRITE_FILE",
    target: "src/Button.jsx",
    shouldAllow: false,
    hook: platformHook,
  },
];
 
async function runTests() {
  let passed = 0;
  let failed = 0;
 
  for (const testCase of testCases) {
    const result = await testCase.hook({
      mode: testCase.mode,
      action: testCase.action,
      target: testCase.target,
      user: "test-user",
      correlationId: "test-123",
    });
 
    const success = result.allowed === testCase.shouldAllow;
 
    if (success) {
      passed++;
      console.log(
        `✅ ${testCase.mode}: ${testCase.action} on ${testCase.target}`,
      );
    } else {
      failed++;
      console.log(
        `❌ ${testCase.mode}: ${testCase.action} on ${testCase.target}`,
      );
      console.log(
        `   Expected allowed=${testCase.shouldAllow}, got ${result.allowed}`,
      );
      console.log(`   Reason: ${result.reason}`);
    }
  }
 
  console.log(`\nResults: ${passed} passed, ${failed} failed`);
}
 
runTests().catch(console.error);

Run this with:

bash
node test/permission-modes.test.mjs

Expected output:

✅ exploration: READ_FILE on src/index.js
✅ exploration: WRITE_FILE on src/index.js
✅ supervised: WRITE_FILE on src/api.js
✅ platform: READ_FILE on terraform/main.tf
✅ platform: WRITE_FILE on src/Button.jsx

Results: 5 passed, 0 failed

Rolling Out Custom Modes

Here's how you actually deploy this in your team. Implementation is just the beginning. Rolling it out successfully means thinking about how people use the system and how you transition to the new modes.

The key is gradualism. You don't flip a switch and force everyone into new permission modes. You introduce them, document them, let people opt in, gather feedback, and refine. You start with the people who need the most specialized permissions first (contractors, new hires) and expand from there.

Documentation is critical. Write down what each mode is for, who should use it, and what they can and can't do. Put it in your team wiki. Make it clear. When someone asks "can I do X?", the answer should be findable in documentation.

  1. Create the config files in your .claude/ directory that define the modes
  2. Write the hooks in .claude/hooks/ that enforce the policies
  3. Test locally before committing to ensure modes work as designed
  4. Document in your team wiki which mode to use for which role:
    • Interns → exploration (read-only learning)
    • Contractors → supervised (full access with audit logs)
    • Platform team → platform (infrastructure-only access)
    • Regular engineers → standard (default mode)
  5. Version control everything — hooks should go through code review like regular code
  6. Audit regularly — check your supervised mode logs monthly to understand actual usage patterns
  7. Gather feedback — talk to people using the modes, understand pain points, iterate
  8. Update documentation as you evolve modes based on real-world usage

The key is that this isn't a one-time setup. You're building governance infrastructure that evolves with your team. Start simple. Use the data from actual usage to inform improvements. Make the system better over time.

The Why Behind the Design

Why layer hooks on top of modes instead of just extending the built-in system?

Composability: You can mix and match. The exploration mode is just "restricted base + read-only hook." If you need a variant, you're tweaking the hook, not redesigning the whole mode. You're not reinventing the wheel for each mode. You're composing behavior from reusable pieces.

Auditability: Each hook is a single file that clearly states what it does. When someone asks "why can't I do X?", you point to the hook and it's obvious. The decision-making logic is transparent and reviewable. There's no magic. It's all code.

Testability: Hooks accept a context object and return a result. No side effects, no magic. You can test them in isolation. You can verify before you deploy that your modes actually do what you intend.

Runtime flexibility: You can change modes without restarting Claude Code. A hook checks the current mode on every action. You can iterate on permission policies without requiring system restarts or deployments.

Maintainability: As your organization evolves and you need new modes or tweaks to existing ones, you're just modifying hooks. You're not touching core permission infrastructure. The changes are contained and low-risk.

This is advanced governance, but it's governance that actually scales. As your team grows and you need more organizational structure encoded into permissions, you just add more hooks. The system stays understandable because each hook is a single policy, clearly expressed.


Iterating and Refining Your Permission Modes

Once you've deployed custom modes, don't expect them to be perfect. Real usage reveals edge cases you didn't anticipate. New roles emerge that don't fit existing modes. Your organization evolves and permission needs shift.

The good news is that because you've separated concerns — configuration files, hooks, tests — you can evolve them independently. Someone from the platform team says "we need to touch this new directory," and you update the allowlist in the platform mode config. Someone in security says "we need to log these actions," and you update the supervised mode hook. No core system changes. No deployments. Just updates to your custom policy layers.

This evolutionary design is actually the strongest argument for layering hooks on modes rather than trying to build perfect modes upfront. You can't predict all your permission needs. So you build a system that's easy to evolve. You start with three modes and gradually add more or refine existing ones as needs emerge.

Monitoring and Auditing Your Modes

Custom permission modes create audit trails and logs. Use them. A platform engineer is blocked from modifying a file? That's logged and worth investigating. A contractor's actions spike dramatically? That might be worth reviewing. Your audit logs are data about how your permission system is actually being used.

Establish a regular review cadence. Monthly, look at your audit logs. Which modes are actually being used? What are the most common blocks and denials? Are there patterns suggesting the wrong people are in the wrong modes? Are there blocked actions that are actually needed and should be allowed?

This feedback loop is how your permission system improves over time. You move from "best guess at permissions" to "data-driven permissions" to "permissions that reflect actual needs."

The Governance Mindset

Building custom permission modes isn't ultimately about technical implementation. It's about governance philosophy. What are you trying to achieve? Are you preventing disasters or enabling people? Are you trying to maintain security or maintain transparency?

The best custom modes balance these tensions. Exploration mode prevents new developers from breaking things while enabling them to learn. Supervised mode maintains audit trails while preserving productivity. Platform mode protects critical infrastructure while empowering specialized teams.

When you're designing your modes, think about the philosophy you're encoding. What does each mode say about your organization's values? Do you trust people? Do you want to catch mistakes or prevent them? Do you assume good intent or assume everything is risky?

Custom permission modes are where governance becomes concrete. They're where your organization's values around trust, safety, and productivity become actual rules that enforce themselves.

Scaling Your Permission System

As your team grows, permission needs become more complex. You might add a QA role that needs special access to testing environments. You might add a security team that audits everything. You might add regional teams that can only touch certain parts of your infrastructure.

Each new need doesn't require rebuilding your system. You add a new mode. You write a hook for it. You test it. You document it. The system scales because it's composable. You're not changing core infrastructure. You're adding new policies on top.

This is the architectural beauty of layering hooks on modes. It supports evolution. Your permission system doesn't have to be perfect at the start. It just has to be composable so you can add policies as needs emerge.

The Governance Maturity Model

Organizations follow a predictable pattern with permissions. They start with binary — locked down or wide open. Then they discover they need something in between. They add custom modes. Then they add more modes. Then they add exception handling. Eventually they have a sophisticated, nuanced permission system that reflects how their organization actually works.

Claude Code supports this evolution. You can start simple with the built-in modes. As you grow, you layer in custom modes. You're never locked into a permission system that doesn't match your needs because you can always extend it.

This is also why custom modes matter. They're not just technical — they're organizational infrastructure. They encode how your team actually works. A junior engineer in exploration mode is reflecting your onboarding strategy. A contractor in supervised mode is reflecting your vendor management strategy. A platform engineer in platform mode is reflecting your organizational structure.

When you design custom modes, you're not just solving a technical problem. You're building the governance infrastructure for how your organization operates at scale.

The Long-Term Perspective

Think of custom permission modes as an investment in your team's future. The effort you put in now to build good modes pays dividends as your team grows. When you onboard your tenth engineer, they use exploration mode and are productive in half the time. When you work with your fifth contractor, the supervised mode logs are already there and auditing is frictionless. When your platform team needs to adjust what they can access, it's just a config change, not a system redesign.

This is where governance becomes an asset rather than a liability. Well-designed permission modes make your team more productive, not less. They enable people to move fast within safe boundaries. They reduce worry about accidental damage. They create clarity about who touches what.

-iNet

Need help implementing this?

We build automation systems like this for clients every day.

Discuss Your Project