Skip to Content
RecipesSandboxed coding assistant

Sandboxed coding assistant

You’ll build a coding assistant that can read and write files only inside one workspace directory, asks you before every write, and records each file access in a signed audit log. Use this pattern whenever an agent edits files on a machine you care about.

import asyncio from pathlib import Path import anyio from loomflow import Agent, Mode, StandardPermissions, tool from loomflow.core.types import ToolCall from loomflow.security import FileAuditLog, FilesystemSandbox from loomflow.tools import InProcessToolHost WORKSPACE = Path("./workspace").resolve() @tool def read_file(path: str) -> str: """Read a file from the workspace.""" return (WORKSPACE / path).read_text() # relative paths open in the workspace @tool(destructive=True) def write_file(path: str, content: str) -> str: """Write content to a file (destructive, so it needs approval).""" (WORKSPACE / path).write_text(content) return f"wrote {len(content)} bytes to {path}" async def confirm_write(call: ToolCall, user_id: str | None) -> bool: question = f"Allow {call.tool} to {call.args.get('path')}? [y/N] " answer = await anyio.to_thread.run_sync(input, question) return answer.strip().lower() == "y" async def main(): inner = InProcessToolHost([read_file, write_file]) sandbox = FilesystemSandbox(inner, roots=[WORKSPACE]) agent = Agent( "You are a coding assistant. Only touch files inside the workspace.", model="claude-opus-4-7", tools=sandbox, permissions=StandardPermissions(mode=Mode.DEFAULT), approval_handler=confirm_write, audit_log=FileAuditLog("./audit.jsonl", secret="prod-secret"), ) await agent.run("Refactor utils.py to use type hints.") asyncio.run(main())

How the layers split the work:

  • FilesystemSandbox rejects any path that resolves outside the workspace, including through a symlink, before the tool runs. It detects path arguments by name (path, file, directory, etc.) and by value (strings containing / or \). It checks a relative path against its first root (base=), so the tools open relative paths there too (WORKSPACE / path) rather than in the process’s working directory. See Sandboxes.
  • Mode.DEFAULT lets read_file run freely and sends every write_file call to the approval handler, which asks in the terminal. Answering anything but y denies the write, and the model sees the refusal on its next turn. The prompt comes before the sandbox check, so approving a path outside the workspace still gets refused.
  • FileAuditLog records each run and tool call with an HMAC signature. See Audit log attribution.

To let the agent write without asking (for example in CI, against a throwaway checkout), switch to Mode.ACCEPT_EDITS and name the tool as a file editor; the sandbox still applies. ACCEPT_EDITS only auto-approves the tools in edit_tools (the built-in write and edit by default), so a custom write_file must be listed:

permissions=StandardPermissions(mode=Mode.ACCEPT_EDITS, edit_tools=["write_file"]),

Any other destructive tool, a shell tool for example, still goes to the approval handler, and is denied when none is wired.

Last updated on