Chapter 8: Permissions and Approval Flow

What Permissions Decide

Permissions answer the question: "Should the agent be allowed to do this now?" That question is more subtle than read versus write. A safe permission system considers:

  • The tool being called.
  • The arguments passed to the tool.
  • The current workspace.
  • The active mode chosen by the user or config.
  • Previously granted allow/deny rules.
  • Hooks or external policy systems.
  • Whether the command can be sandboxed.

A Generic Permission Engine

class PermissionEngine:
    def authorize(self, action, context):
        if self.matches_deny_rule(action):
            return Decision.deny("matched deny rule")

        hook_decision = self.run_pre_permission_hooks(action)
        if hook_decision is not None:
            return hook_decision

        required = classify_required_access(action)

        if self.mode_satisfies(required, context.mode):
            return Decision.allow()

        if context.mode == "prompt":
            return ask_user(action, required)

        return Decision.deny(f"requires {required}")

The quality of the system depends heavily on classify_required_access.

Codex: Approval Policy And Orchestration

Codex routes execution through an approval and orchestration layer. Tools can declare or compute their approval requirements. Shell and patch tools are then run under the selected approval mode, sandbox mode, hooks, and optional retry-with-escalation path.

Codex Approval Shape

async def codex_authorize_execution(request, turn):
    requirement = default_exec_approval_requirement(
        request=request,
        approval_policy=turn.approval_policy,
        sandbox_policy=turn.sandbox_policy,
    )

    hook_result = await run_permission_hooks(request)
    if hook_result is not None:
        return hook_result

    if requirement == "allow":
        return Decision.allow()

    if requirement == "ask_user":
        return await turn.session.ask_for_approval(request)

    if requirement == "deny":
        return Decision.deny("policy denied execution")

Codex approval is tightly coupled to sandboxing. If a command fails because the sandbox blocked it, the orchestrator may ask whether to retry with broader permissions, depending on the active policy.

Codex Permission Modes

The exact names and config surfaces evolve, but the core ideas are stable:

  • Never ask and run within existing constraints.
  • Ask on request or for risky operations.
  • Ask after sandbox failure before retrying without containment.
  • Apply granular filesystem and network permissions.
  • Use external review or guardian-like checks where configured.

Claw: Permission Modes And Rules

Claw has a direct permission model with modes such as:

Mode Meaning
Read-only Allow safe reads, deny writes and risky execution
Workspace-write Allow writes under the workspace
Danger-full-access Allow broad execution
Prompt Ask when the policy cannot auto-allow
Allow Used for explicit allow behavior in policy paths

Rules can allow, deny, or ask for specific tools or patterns. The enforcer wraps the policy and provides helper checks for file writes and shell commands.

Claw Authorization Shape

async def claw_authorize(action, policy, prompter=None):
    if policy.matches_deny(action):
        return Decision.deny("denied by rule")

    if policy.matches_allow(action):
        return Decision.allow()

    if policy.matches_ask(action):
        if prompter is None:
            return Decision.deny("approval required but no prompter")
        return await prompter.ask(action)

    required_mode = classify_required_mode(action)

    if policy.active_mode_allows(required_mode):
        return Decision.allow()

    if policy.active_mode == "prompt" and prompter is not None:
        return await prompter.ask(action)

    return Decision.deny(f"requires {required_mode}")

The rule ordering matters. Deny rules should win before broad allow modes.

Command Classification

Shell commands are hard because the string can hide many effects:

def classify_shell_command(command):
    read_only_patterns = [
        "ls", "pwd", "cat", "sed -n", "rg", "git status", "git diff",
    ]
    dangerous_patterns = [
        "rm -rf", "curl | sh", "chmod -R", "git push", "sudo",
    ]

    if contains_any(command, dangerous_patterns):
        return "dangerous"

    if starts_with_any(command, read_only_patterns):
        return "read"

    if looks_like_test_or_build(command):
        return "workspace_write"  # build artifacts may be written

    return "ask"

No heuristic is perfect. Codex reduces risk with sandboxing. Claw reduces risk with explicit modes, rules, and structured alternatives to shell commands.

File Permission Classification

File actions are easier to classify than shell actions:

def classify_file_action(action, workspace):
    path = canonicalize(action.path)

    if action.kind == "read":
        return "read"

    if action.kind in {"write", "edit", "delete"}:
        if path.is_relative_to(workspace.root):
            return "workspace_write"
        return "danger_full_access"

    return "ask"

This is why dedicated file tools can be safer than arbitrary shell commands. Their arguments have structure.

Asking The User

Permission prompts should be specific. A bad prompt asks, "Allow command?" A good prompt explains the tool, target, expected effect, and risk.

def render_permission_prompt(action):
    return {
        "title": f"Allow {action.tool_name}?",
        "target": action.target_summary(),
        "effect": action.effect_summary(),
        "risk": action.risk_summary(),
        "choices": ["allow once", "allow always", "deny"],
    }

Claw's prompt mode naturally fits this model. Codex also surfaces approval requests through its session/event protocol.

Hooks And External Policy

Both systems support hook-like extension points around tool execution. Hooks are important because not every organization wants the same policy.

async def run_policy_hooks(action):
    for hook in configured_hooks("pre_tool_use"):
        result = await hook(action)
        if result in {"allow", "deny"}:
            return Decision.from_hook(result)
    return None

Hooks can enforce repository-specific restrictions, audit commands, deny deployment actions, or require approval for sensitive files.

Permission Hooks Versus Tool Hooks

Permission hooks and tool hooks should be treated as different phases.

async def authorize_then_execute(call):
    permission_hook = await hooks.run("permission_request", call)
    if permission_hook.denied:
        return denied(permission_hook.reason)

    decision = await permission_engine.authorize(call)
    if not decision.allowed:
        return denied(decision.reason)

    pre_tool = await hooks.run("pre_tool_use", call)
    if pre_tool.denied:
        return denied(pre_tool.reason)

    result = await tool.run(call)
    await hooks.run("post_tool_use", call=call, result=result)
    return result

The permission phase decides whether the action may happen. The pre/post tool phases observe or refine an already authorized execution path. Keeping those phases separate avoids confusing "policy said yes" with "a hook happened to let the command run."

Comparison

Aspect Codex Claw
Permission center Approval requirement plus orchestrator Permission policy plus enforcer
Strongest safety partner Sandbox manager Structured tool classification
User prompt path Session approval events Prompt mode/propmter path
Shell command handling Policy plus sandbox retry logic Heuristic classification plus mode checks
File handling Patch/file targets routed through approval Workspace boundary checks in file tools
Extensibility Hooks, guardian/review paths, config policy Rules, hooks, tool requirements

Practical Lessons

  • Deny rules should override broad allow modes.
  • Shell commands need conservative classification.
  • File tools are safer when they receive structured paths.
  • A permission denial should become model-visible feedback, not a crash.
  • Approval prompts should describe effect, not just syntax.
  • Permissions and sandboxing work best together.

Source Anchors

For Codex, useful filenames are orchestrator.rs, sandboxing.rs, request_permissions.rs, and shell.rs. For Claw, useful filenames are permissions.rs, permission_enforcer.rs, bash.rs, and file_ops.rs.