Skip to content

Feature: Path-based Access Control Rules #513

@austinm911

Description

@austinm911

Human Summary

I think this would be a nice to have for users who want to give local device access for features like iMessage and so forth to their agent. Maybe you don't want someone you trust in a group chat/channel being able to easily discover everything on your device. I'm assuming most users who share access to a family member(s) or close colleagues, these folks won't be as technical to totally understand how to prompt inject their way around these access controls. Something like https://github.com/Dicklesworthstone/acip/tree/main/integrations/clawdbot could also be added by user to add more protection.

But hopefully, something like this would act as a 1st layer of defense.

Summary

Add granular path-based access control that can deny or allow specific file paths/patterns for read, write, edit, and bash operations. Supports global rules with per-agent overrides.

Motivation

Currently Clawdbot has:

  • Tool-level allow/deny — block entire tools (e.g., deny: ["write"])
  • Sandbox root containment — prevent escaping sandbox directory

What's missing: No way to restrict access to specific paths within the allowed scope. Users cannot:

  • Block ~/.ssh/** while allowing everything else
  • Deny .env files globally but allow a specific agent to access them
  • Protect ~/Documents/taxes/** from all agents

Proposed Design

Config Schema

// ~/.clawdbot/clawdbot.json
{
  access: {
    // Self-contained rules, each with its own effect + pattern + operations
    rules: [
      { deny: "~/.ssh/**",       ops: ["*"] },
      { deny: "~/.gnupg/**",     ops: ["*"] },
      { deny: "**/.env",         ops: ["read", "write", "edit"] },
      { deny: "**/secrets/**",   ops: ["*"] },
      
      // Specific allows can override denies
      { allow: "~/.ssh/config",  ops: ["read"] },
    ],
    
    // How to resolve conflicts (default: "deny-wins")
    mode: "deny-wins"
  },

  routing: {
    agents: {
      main: {
        // Agent-specific rules (evaluated before global)
        access: {
          rules: [
            { deny: "~/work/client/**", ops: ["*"] }
          ]
        }
      },
      
      "secrets-manager": {
        // This agent CAN access globally-denied paths
        access: {
          rules: [
            { allow: "**/.env", ops: ["read", "write", "edit"] },
            { allow: "~/.gnupg/**", ops: ["read"] }
          ]
        }
      }
    }
  }
}

Shorthand Syntax

For common cases:

{
  access: {
    // Shorthand: string array = deny all ops for these patterns
    deny: ["~/.ssh/**", "~/.gnupg/**", "**/.env"],
    
    // Fine-grained rules when needed
    rules: [
      { allow: "~/.ssh/config", ops: ["read"] }
    ]
  }
}

Pattern Syntax

  • Glob (default): ~/.ssh/**, *.env, **/secrets/*
  • Regex (wrapped in slashes): /\.env$/, /secrets\//

Operations

Op Affected Tools
read read tool
write write tool
edit edit tool
bash bash tool (heuristic path extraction)
* All of the above

Resolution Logic (deny-wins mode)

For a given (agent, path, operation):

1. If agent rule explicitly ALLOWs → ALLOW (agent override)
2. If agent rule explicitly DENYs → DENY
3. If global rule DENYs → DENY
4. Otherwise → ALLOW

Alternative first-match mode evaluates rules top-to-bottom, first match wins.

Implementation Plan

Phase 1: Clawdbot Tool Wrapper (this PR)

Extend the existing wrapSandboxPathGuard() pattern in pi-tools.ts:

function wrapAccessGuard(
  tool: AnyAgentTool, 
  accessRules: AccessRule[],
  operation: AccessOperation
): AnyAgentTool {
  return {
    ...tool,
    execute: async (toolCallId, args, signal, onUpdate) => {
      const filePath = extractPathFromArgs(args);
      if (filePath) {
        const result = evaluateAccess(filePath, operation, accessRules);
        if (result.denied) {
          return {
            content: [{ type: "text", text: `Access denied: ${result.reason}` }],
            isError: true,
          };
        }
      }
      return tool.execute(toolCallId, args, signal, onUpdate);
    },
  };
}

Phase 2: Bash Path Extraction

Extract and check paths from bash commands against deny rules:

Pattern Examples
File commands cat, less, head, tail, grep, sed, rm, mv, cp, chmod
Redirects > file, >> file, < file
Path-like args Anything starting with /, ~/, ./, ../

Best-effort — see Limitations section for bypass vectors.

Phase 3: Future — Pi Extension (upstream)

Pi's extension API already has tool_call event with blocking:

api.on("tool_call", (event, ctx) => {
  const path = event.input.path;
  if (isPathDenied(path)) {
    return { block: true, reason: "Access denied by policy" };
  }
});

Could propose this as a Pi core feature. Clawdbot would then just inject config.

Files to Modify

File Change
src/agents/sandbox.ts Add AccessRule type, evaluateAccess() function
src/config/types.ts Add AccessConfig to agent and global config
src/config/zod-schema.ts Add Zod schema for access rules
src/agents/pi-tools.ts Add wrapAccessGuard(), apply to read/write/edit tools
src/agents/bash-tools.ts Add path extraction heuristics for bash commands
docs/gateway/security.md Document access control feature
docs/gateway/configuration.md Add config reference

Existing Patterns to Follow

The codebase already has clean patterns for this:

Tool wrapping (pi-tools.ts:472-488):

function wrapSandboxPathGuard(tool: AnyAgentTool, root: string): AnyAgentTool {
  return {
    ...tool,
    execute: async (toolCallId, args, signal, onUpdate) => {
      const filePath = record?.path;
      if (typeof filePath === "string" && filePath.trim()) {
        await assertSandboxPath({ filePath, cwd: root, root });
      }
      return tool.execute(toolCallId, args, signal, onUpdate);
    },
  };
}

Policy checking (pi-tools.ts:454-469):

function isToolAllowedByPolicy(name: string, policy?: SandboxToolPolicy) {
  if (!policy) return true;
  const deny = new Set(normalizeToolNames(policy.deny));
  const allow = allowRaw.length > 0 ? new Set(allowRaw) : null;
  if (deny.has(normalized)) return false;
  if (allow) return allow.has(normalized);
  return true;
}

Limitations & Threat Model

This is a policy boundary, not a security sandbox.

Protects Against Does NOT Protect Against
Accidental access (model mistakes) Encoded/obfuscated paths (base64, xxd)
Clear policy signal to model Interpreter bypass (python -c "open(...)")
Audit trail of denied attempts Multi-step attacks (write script → execute)
Defense in depth Network exfiltration

Bash extraction is best-effort. We parse common patterns (cat, less, grep, redirects, rm, mv, cp) but cannot catch:

  • Creative commands (dd if=..., tar, custom binaries)
  • Scripting language one-liners
  • Commands written to a script then executed

For true isolation: Use Docker sandbox with workspaceAccess: "none" and network restrictions. Access control complements sandboxing—it doesn't replace it.

Open Questions

  1. Default blocks? Ship with ~/.ssh, ~/.gnupg, .env, secrets/ blocked by default? (with useDefaults: false escape hatch)

  2. Error message verbosity — Show blocked path in error? Or keep vague?

    • Verbose: "Access denied: ~/.ssh/id_rsa blocked by policy"
    • Vague: "Access denied by policy"

References

  • AWS IAM policy model (Effect + Action + Resource per statement)
  • Kubernetes RBAC (verbs + resources per rule)
  • Existing SandboxToolPolicy pattern in this codebase

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions