Python ToolPermissionContext

The Python-side permission system centers on the ToolPermissionContext dataclass, which carries two fields for blocking tools:

class ToolPermissionContext: deny_names: frozenset[str] # exact tool names to block deny_prefixes: tuple[str, ...] # prefix patterns to block

deny_names is a frozenset of exact tool names that should be blocked. deny_prefixes is a tuple of string prefixes — any tool whose name starts with a listed prefix is blocked. Both fields are constructed via the from_iterables class method, which lowercases all entries to ensure case-insensitive matching.

The blocking check is implemented in the blocks() method:

def blocks(self, tool_name: str) -> bool: name_lower = tool_name.lower() if name_lower in self.deny_names: return True return any( name_lower.startswith(prefix) for prefix in self.deny_prefixes )

The method first checks for an exact match against deny_names, then checks whether the lowercased tool name starts with any entry in deny_prefixes. This two-stage check is efficient: the frozenset lookup is O(1), and the prefix scan is O(n) over a typically small prefix list.

Rust 권한 모델

The Rust layer implements a richer permission model with three distinct modes:

enum PermissionMode { Allow, // tool is always permitted Deny, // tool is always blocked Prompt, // user is asked for each invocation }

The Prompt mode is the key differentiator from the Python layer. Instead of a binary allow/deny decision, the Rust permission system can pause execution and ask the user whether to allow a specific tool invocation. This is mediated through the PermissionPrompter trait:

struct PermissionRequest { tool_name: String, input: String, } enum PermissionPromptDecision { Allow, Deny { reason: String }, } trait PermissionPrompter { fn decide(&mut self, request: PermissionRequest) -> PermissionPromptDecision; }

The PermissionRequest includes both the tool name and the full input string, so the user can make an informed decision about whether to allow the specific operation. The PermissionPromptDecision enum includes an optional reason on denial, which is fed back to the model so it can understand why its tool use was rejected.

PermissionPolicy

The PermissionPolicy struct ties everything together. It defines a default_mode (applied to any tool without a specific override) and a tool_modes map (BTreeMap<String, PermissionMode>) for per-tool overrides:

struct PermissionPolicy { default_mode: PermissionMode, tool_modes: BTreeMap<String, PermissionMode>, }

The policy is constructed with new(default_mode) and customized with with_tool_mode(tool_name, mode) for individual tools. The mode_for(tool_name) method looks up the specific mode for a tool, falling back to the default if no override exists.

The authorize(tool_name, input, prompter) method executes the full authorization flow:

  1. Look up the mode for the tool via mode_for
  2. If Allow, return PermissionOutcome::Allow immediately
  3. If Deny, return PermissionOutcome::Deny with a static reason
  4. If Prompt, construct a PermissionRequest and call prompter.decide()
  5. Map the prompter's decision to a PermissionOutcome

The PermissionOutcome enum mirrors the decision types: Allow or Deny{reason}. This separation between policy (what mode applies) and prompting (how the user is asked) keeps the system testable — in tests, the prompter can be a mock that always allows or always denies.

CLI 권한 모드

The CLI exposes three preset permission modes via args.rs, providing convenient defaults for common use cases:

기본 모드

WorkspaceWrite is the default for a reason — it provides enough capability for real development work while keeping the sandbox boundary intact. ReadOnly is useful for code review and exploration. DangerFullAccess should only be used in controlled environments where the agent is trusted.

도구 필터링

Permission filtering happens before tools reach the agent's context window. The get_tools function accepts a permission_context parameter and calls filter_tools_by_permission_context to remove any blocked tools from the list. This ensures that denied tools are never described in the system prompt — the agent literally does not know they exist.

This design is more secure than post-hoc enforcement. If a blocked tool appeared in the prompt but was rejected at execution time, the agent would waste turns attempting to use it and might try to work around the restriction. By filtering at the prompt level, the agent's decision-making is constrained to only the tools it can actually use.

심플 모드

Simple mode is the most restrictive tool configuration. When enabled, the tool pool is reduced to exactly three tools:

All other tools — web access, sub-agents, search, notebooks, task management — are removed. This provides a minimal, predictable surface area for constrained environments where the full 19-tool suite would be excessive.

런타임 권한 추론

The runtime layer adds a final permission check via _infer_permission_denials. This function gates bash-like tools (any tool that can execute arbitrary commands) based on the active CLI permission mode. Even if a tool passes the ToolPermissionContext filter, the runtime inference layer can still block it based on the broader session permissions.

This layered approach — Python deny-list filtering, Rust policy engine, CLI mode presets, and runtime inference — creates defense in depth. Each layer catches cases the others might miss, ensuring that the claw code permission system is robust against configuration errors and edge cases.

보안 모델 요약

The claw code permission system provides per-tool granularity through four layers: Python deny lists (fast filtering), Rust permission policies (fine-grained control with prompting), CLI mode presets (convenient defaults), and runtime inference (final gating). Tools blocked at any layer never reach the agent.