Skip to content

Add sandbox and permission mode configuration for orchestrated agents #78

@geoffjay

Description

@geoffjay

Context

Claude Code's Bash tool runs commands inside a macOS sandbox-exec sandbox by default, restricting file system and network access. Additionally, Claude Code supports several permission and tool restriction modes via CLI flags:

  • --permission-mode <mode> — controls how tool permissions are handled (default, plan, acceptEdits, dontAsk, bypassPermissions)
  • --dangerously-skip-permissions — bypasses all permission checks (intended for sandboxed environments)
  • --allowed-tools <tools> / --disallowed-tools <tools> — whitelist/blacklist specific tools
  • --tools <tools> — restrict the available tool set entirely (e.g., "Read,Grep,Glob" for read-only)
  • --settings <json> — pass a settings file or JSON string to configure the session

Currently, the orchestrator's build_claude_command() function (in crates/orchestrator/src/manager.rs) constructs the claude CLI invocation but does not pass any of these security-related flags. All agents run with default permissions, meaning:

  • Sandbox is on by default for Bash (good), but agents can request dangerouslyDisableSandbox: true via the Bash tool parameter
  • There's no way to enforce --permission-mode plan or restrict the tool set
  • The /sandbox slash command is an interactive-mode feature that cannot be used in SDK mode (--sdk-url)

The Problem

Users want to create agents with explicit security postures — e.g., a read-only agent that can only use Read/Grep/Glob, or an agent in plan mode that can explore but not modify code, or a fully sandboxed agent with --dangerously-skip-permissions (paradoxically, this is the recommended mode for sandboxed environments because the sandbox itself provides the restriction).

Proposal

Add permission_mode, allowed_tools, disallowed_tools, and sandbox configuration to agent creation, and pass the corresponding CLI flags when launching Claude.

Agent Config Changes

// In crates/orchestrator/src/types.rs
pub struct AgentConfig {
    // ... existing fields ...

    /// Permission mode for the claude session.
    /// Maps to --permission-mode flag.
    /// Options: "default", "plan", "acceptEdits", "dontAsk", "bypassPermissions"
    #[serde(skip_serializing_if = "Option::is_none")]
    pub permission_mode: Option<String>,

    /// Allowed tools whitelist.
    /// Maps to --allowed-tools flag.
    /// e.g., ["Bash(git:*)", "Read", "Grep", "Glob"]
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub allowed_tools: Vec<String>,

    /// Disallowed tools blacklist.
    /// Maps to --disallowed-tools flag.
    /// e.g., ["Bash", "Write", "Edit"]
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub disallowed_tools: Vec<String>,

    /// Restrict to specific built-in tools only.
    /// Maps to --tools flag.
    /// e.g., ["Read", "Grep", "Glob", "WebFetch"]
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tools: Vec<String>,

    /// Enable --dangerously-skip-permissions.
    /// Intended for use WITH sandbox mode — the sandbox provides the security
    /// boundary, and skipping permissions avoids the agent blocking on prompts.
    #[serde(default)]
    pub skip_permissions: bool,
}

build_claude_command Changes

// In crates/orchestrator/src/manager.rs
fn build_claude_command(config: &AgentConfig, ws_url: &str) -> String {
    let mut args = vec!["claude".to_string()];

    if !config.interactive {
        args.push(format!("--sdk-url {}", ws_url));
        args.push("--print".to_string());
        args.push("--output-format stream-json".to_string());
        args.push("--input-format stream-json".to_string());
    }

    // Security configuration
    if let Some(ref mode) = config.permission_mode {
        args.push(format!("--permission-mode {}", mode));
    }

    if !config.allowed_tools.is_empty() {
        args.push(format!("--allowed-tools {}", config.allowed_tools.join(" ")));
    }

    if !config.disallowed_tools.is_empty() {
        args.push(format!("--disallowed-tools {}", config.disallowed_tools.join(" ")));
    }

    if !config.tools.is_empty() {
        args.push(format!("--tools {}", config.tools.join(",")));
    }

    if config.skip_permissions {
        args.push("--dangerously-skip-permissions".to_string());
    }

    // ... existing worktree, system_prompt, user handling ...
}

CLI Changes

# Create a read-only agent
agent orchestrator create-agent \
  --name reviewer \
  --working-dir /path/to/project \
  --tools "Read,Grep,Glob,WebFetch" \
  --permission-mode plan

# Create a fully sandboxed agent (skip permissions, rely on sandbox)
agent orchestrator create-agent \
  --name worker \
  --working-dir /path/to/project \
  --skip-permissions \
  --disallowed-tools "Bash(rm:*),Bash(sudo:*)"

# Create an agent that can only use git and read operations
agent orchestrator create-agent \
  --name safe-worker \
  --working-dir /path/to/project \
  --allowed-tools "Bash(git:*),Bash(cargo:*),Read,Grep,Glob,Edit,Write"

YAML Template Support (from #72)

# .agentd/agents/reviewer.yml
name: reviewer
working_dir: "."
permission_mode: plan
tools:
  - Read
  - Grep
  - Glob
  - WebFetch
system_prompt: |
  You are a code reviewer. Read and analyze code, but do not modify anything.
# .agentd/agents/sandboxed-worker.yml
name: sandboxed-worker
working_dir: "."
skip_permissions: true
disallowed_tools:
  - "Bash(rm:*)"
  - "Bash(sudo:*)"
system_prompt: |
  You are a worker agent. The sandbox restricts your file and network access.

Acceptance Criteria

  • Add permission_mode, allowed_tools, disallowed_tools, tools, and skip_permissions fields to AgentConfig
  • Add corresponding fields to CreateAgentRequest
  • Update build_claude_command() to pass the correct CLI flags
  • Add CLI flags: --permission-mode, --allowed-tools, --disallowed-tools, --tools, --skip-permissions
  • Add validation: warn if --skip-permissions is used without explaining sandbox intent
  • Persist security config in the agents SQLite table
  • Display security configuration in get-agent and list-agents output
  • Add tests for build_claude_command() with various security configurations
  • Document security modes and recommended configurations in README

Interaction with Tool Policy (#67, #68)

This feature and the orchestrator-level ToolPolicy (#67) provide defense in depth:

  • Claude-level restrictions (this issue): --tools, --allowed-tools, --permission-mode — restrict what the Claude model can even attempt to invoke
  • Orchestrator-level ToolPolicy (Merge and iterate on ToolPolicy enforcement (PR #34) #67): intercepts can_use_tool control requests and allows/denies/queues for approval at the WebSocket protocol level

Both layers should be configurable independently. For maximum security, use both:

  1. --tools "Read,Grep,Glob,Bash" (Claude-level: only these tools exist)
  2. ToolPolicy AllowList ["Read", "Grep", "Glob", "Bash(git:*)"] (Orchestrator-level: further restricts Bash usage patterns)

Relevant Files

  • crates/orchestrator/src/types.rsAgentConfig, CreateAgentRequest
  • crates/orchestrator/src/manager.rsbuild_claude_command()
  • crates/orchestrator/src/storage.rs — agent persistence
  • crates/cli/src/commands/orchestrator.rsCreateAgent command
  • scripts/launch-planner.sh, scripts/launch-worker.sh — examples that could use these flags

Notes

  • The macOS sandbox-exec sandbox is always on by default for the Bash tool in Claude Code — this issue is about configuring the permission model and tool restrictions that layer on top of it
  • --dangerously-skip-permissions sounds dangerous but is actually the recommended approach for sandboxed agents: the sandbox provides the security boundary, and skipping permissions prevents the agent from blocking indefinitely on permission prompts that no human will answer
  • Interactive mode (config.interactive = true) agents don't use these flags since the user interacts directly

Dependencies

Related: #67 (orchestrator-level ToolPolicy provides defense-in-depth alongside Claude-level sandbox restrictions), #72 (YAML agent templates should support permission_mode/tools fields)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions