Skip to main content
Claw Code gates tool execution through a lightweight deny-list system. Before any tool is dispatched, it is checked against a ToolPermissionContext that can block tools by exact name or by name prefix.

ToolPermissionContext

ToolPermissionContext (in permissions.py) is an immutable dataclass that holds two deny collections:
@dataclass(frozen=True)
class ToolPermissionContext:
    deny_names: frozenset[str]    # exact lowercase tool names to block
    deny_prefixes: tuple[str, ...]  # lowercase prefixes; any matching tool is blocked

Creating a context

Use from_iterables() to construct a context from plain lists. All names and prefixes are normalised to lowercase:
@classmethod
def from_iterables(
    cls,
    deny_names: list[str] | None = None,
    deny_prefixes: list[str] | None = None,
) -> 'ToolPermissionContext':
    return cls(
        deny_names=frozenset(name.lower() for name in (deny_names or [])),
        deny_prefixes=tuple(prefix.lower() for prefix in (deny_prefixes or [])),
    )

The blocks() method

blocks(tool_name) returns True if the tool should be denied:
def blocks(self, tool_name: str) -> bool:
    lowered = tool_name.lower()
    return lowered in self.deny_names or any(
        lowered.startswith(prefix) for prefix in self.deny_prefixes
    )
A tool is blocked if its lowercase name appears in deny_names or starts with any entry in deny_prefixes. Prefix matching lets you deny entire families of tools (for example, all tools whose names start with mcp_) without listing each one.

PermissionDenial

When a tool is blocked, a PermissionDenial is recorded:
@dataclass(frozen=True)
class PermissionDenial:
    tool_name: str
    reason: str
Denials accumulate in QueryEnginePort.permission_denials and are surfaced in each TurnResult.permission_denials.

Default behaviour: bash tool gating

PortRuntime._infer_permission_denials() applies an automatic denial rule that does not require any explicit configuration: any matched tool whose name contains bash (case-insensitive) is denied before it reaches the engine.
def _infer_permission_denials(self, matches: list[RoutedMatch]) -> list[PermissionDenial]:
    denials: list[PermissionDenial] = []
    for match in matches:
        if match.kind == 'tool' and 'bash' in match.name.lower():
            denials.append(PermissionDenial(
                tool_name=match.name,
                reason='destructive shell execution remains gated in the Python port',
            ))
    return denials
Bash tool gating is unconditional in the current Python port. Even if BashTool is present in the active ToolPool, it will be denied at runtime. This mirrors the conservative trust model of the original TypeScript harness during early porting.

How denials flow through a session

1

Prompt routing

PortRuntime.route_prompt() scores and selects the top-matching commands and tools from the inventories.
2

Denial inference

_infer_permission_denials() inspects the matched tools and builds a list of PermissionDenial objects.
3

Engine submission

The denials are passed to both stream_submit_message() and submit_message() as denied_tools.
4

Stream event emitted

If any denials are present, the stream emits a permission_denial event before the response delta:
yield {'type': 'permission_denial', 'denials': [denial.tool_name for denial in denied_tools]}
5

TurnResult

Denials are recorded in TurnResult.permission_denials and in the engine’s own permission_denials list, so they are available for inspection after the turn completes.

CLI flags

Apply per-invocation deny rules using the tools subcommand:
# Deny a specific tool by exact name
python3 -m src.main tools --deny-tool BashTool

# Deny all tools whose names start with a prefix
python3 -m src.main tools --deny-prefix mcp_

# Combine multiple deny rules
python3 -m src.main tools --deny-tool BashTool --deny-prefix mcp_ --deny-prefix experimental_
In Python, build the context directly and pass it to get_tools() or assemble_tool_pool():
from src.permissions import ToolPermissionContext
from src.tools import get_tools

ctx = ToolPermissionContext.from_iterables(
    deny_names=['BashTool'],
    deny_prefixes=['mcp_'],
)
active_tools = get_tools(permission_context=ctx)
--deny-tool and --deny-prefix can be repeated any number of times on the command line. Each occurrence appends to the respective list.

Inspecting denials after a session

After running a session, denied tools appear in the TurnResult:
result = engine.submit_message(prompt, matched_tools=tool_names, denied_tools=denials)

for denial in result.permission_denials:
    print(f'{denial.tool_name}: {denial.reason}')
The engine also tracks cumulative denials across all turns in engine.permission_denials, which is included in the output of render_summary().

Build docs developers (and LLMs) love