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}
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.