Skip to content
C.W.K.
Stream
Lesson 06 of 06 · published

Tool Security: Permissions, Sandboxes, and Side Effects

~14 min · security, permissions, side-effects

Level 0Observer
0 XP0/64 lessons0/13 achievements
0/150 XP to next level150 XP to go0% complete

Tool descriptions are public

Anything you write in a tool description goes to Anthropic on every call that mentions the tool. That includes hostnames, internal paths, table names, secret hints. Strip secrets before deploying — descriptions are not a private notes field.

Side effects need explicit permission

Read tools (search, lookup, get) can usually run unattended. Write tools (create, modify, delete) deserve a human-in-the-loop check. The Agent SDK supports this natively via permission prompts; on the Client SDK you build it yourself. Either way, never let a model trigger destructive side effects without acknowledgment.

Sandbox the dangerous

If a tool runs shell commands, executes code, or hits the filesystem, it lives in a sandbox: restricted environment variables, path arguments checked against one root and flags limited to a known list (a working directory alone stops neither absolute paths nor .., and flags such as grep -R or -f<file> reach around a path check), a time budget, an output size cap. A command-name allowlist is a guard, not a sandbox; untrusted input also needs the OS to confine the process. Anthropic's code_execution tool runs in a sandbox; your custom tools must match.

Principle: Tools have authority. Audit every tool the same way you would audit a new API endpoint exposed to untrusted users.

Code

Permission gate around a write tool·python
import os, json

DESTRUCTIVE = {"delete_file", "drop_table", "send_email"}

async def gated_invoke(block, ask_user):
    args_str = json.dumps(block.input, indent=2)
    if block.name in DESTRUCTIVE:
        approved = await ask_user(f"Allow {block.name} with args:\n{args_str}")
        if not approved:
            return {"type": "tool_result", "tool_use_id": block.id,
                    "is_error": True, "content": "user denied permission"}
    handler = HANDLERS[block.name]
    out = await handler(**block.input)
    return {"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(out)}
Allowlisted shell tool: path arguments checked against one root, known flags only, a time budget·python
import asyncio, os, shlex

ROOT = os.path.realpath("/sandbox")
ALLOWED = {"ls", "cat", "grep", "head", "tail"}
FLAGS = {"-l", "-a", "-la", "-1", "-n", "-i", "-c", "-v", "-w", "-E", "-F", "-r", "-h"}

def inside_root(arg: str) -> bool:
    # absolute paths, .. and symlinks all resolve before the comparison
    path = os.path.realpath(os.path.join(ROOT, arg))
    return path == ROOT or path.startswith(ROOT + os.sep)

async def guarded_shell(command: str, timeout: float = 5.0):
    parts = shlex.split(command)
    if not parts or parts[0] not in ALLOWED:
        raise PermissionError(f"command not in allowlist: {parts[0] if parts else ''}")
    # cwd alone confines nothing: `cat /etc/passwd` and `cat ../x` would still run.
    for arg in parts[1:]:
        if arg.startswith("-"):
            # known standalone flags only: no -R/-S (follow symlinks), no -f<file>, no --opt=path
            if arg not in FLAGS:
                raise PermissionError(f"flag not allowed: {arg}")
        elif not inside_root(arg):
            raise PermissionError(f"path escapes the sandbox: {arg}")
    proc = await asyncio.create_subprocess_exec(
        *parts,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
        cwd=ROOT,
        env={"PATH": "/usr/bin:/bin"},  # minimal env
    )
    try:
        out, err = await asyncio.wait_for(proc.communicate(), timeout=timeout)
    except asyncio.TimeoutError:
        proc.kill()
        raise TimeoutError("command exceeded time budget")
    return {"stdout": out.decode()[:8192], "stderr": err.decode()[:2048], "returncode": proc.returncode}

# This is a guard, not a sandbox: for untrusted input, also run it where the OS
# confines the process (a container, a VM, macOS sandbox-exec).

External links

Exercise

Audit one tool in your project that has side effects. Add a permission gate, a sandbox boundary, and an audit log entry. Verify each by writing a test that proves the tool refuses without permission.
Hint
If your tool can drop a database table without confirmation, you have a production incident waiting.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.