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, working directory whitelist, time budget, output size cap. 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)}
Sandboxed shell tool with a time budget·python
import asyncio, shlex

ALLOWED = {"ls", "cat", "grep", "head", "tail"}

async def safe_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 ''}")
    proc = await asyncio.create_subprocess_exec(
        *parts,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
        cwd="/sandbox",  # whitelist working directory
        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}

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 inPlease sign in to comment.

No comments yet — be the first.