The Letta agent harness, letta-code, runs client-side tools on the user’s machine while the model decides what to ask for next. That split keeps shell state, local files, and scoped memory inside the client boundary, where the harness can enforce permissions and sandboxing before any action reaches a tool. This page follows that path from turn orchestration to execution and back again.
The design choice
The streamed turn listener in src/websocket/listener/turn.ts owns the full loop, and it hands approval stops to src/websocket/listener/turn-approval.ts when a tool call needs a pause. The listener also carries the current permission mode through per-conversation state, so the model, the approval path, and the executor all read the same posture during one turn. See Anatomy of a turn
for the broader turn lifecycle.
In v1, the server executed tools; v2 inverted that boundary so local and private environments keep tool side effects on the user’s machine.
The tool layer
The central tool assembly path runs through src/tools/manager.ts and src/tools/toolset.ts. Those files gather the built-in catalog, layer in the descriptions under src/tools/descriptions/, and let src/mods/tool-registry.ts extend the surface.
The concrete implementations live under src/tools/impl/.
- Execution tools:
BashandShelllaunch terminal commands and carry the heavy work of package installs, scripts, and other shell tasks. - File tools:
Read,Edit, andWritehandle direct file work, while the path-oriented search helpers support lookup and targeting. - Search tools:
Grepcovers content search, and the related path helpers fill the same role when a path filter helps more than a command line. - Memory tools:
memoryandmemory_apply_patchoperate on the scoped memory filesystem; see Memory blocks and the memory filesystem . - Delegation tools:
Tasklaunches subagents, andMessageChannelsends replies back through external channels when a notification arrives. - Skills: the skill layer stays adjacent to this page, and queued skill content enters the stream through
src/websocket/listener/skill-injection.tsrather than through a separate memory system. - Mods: extra tools extend the surface without changing the core catalog.
For the larger extension model around skills, subagents, and mods, see Skills, subagents, and mods .
The gate sequence
The listener treats a streamed tool call as a gated pipeline. The permission decision comes first, approval pauses come next when needed, and PreToolUse hooks run only at the start of the execution closure after that decision resolves. Sandboxing still wraps the process that actually executes, and post-hooks can add context after the tool returns.
flowchart LR
Stream[Streamed tool call] --> Perm[Permission check]
Perm --> NeedApproval{Need approval?}
NeedApproval -->|yes| Approval[Approval listener pause]
NeedApproval -->|no| PreHooks[Pre hooks]
Approval --> PreHooks
PreHooks --> Execute[Sandboxed execution]
Execute --> PostHooks[Post hooks]
PostHooks --> Engine[Result back to engine]Permission check
checkToolPermission() computes the decision before execution and classifies whether the call can run or must pause. checkPermissionWithHooks() can still turn an ask into an allow or a deny through permission-request hooks, but the cross-agent guard in src/permissions/checker.ts stays the first and strongest deny.
Approval pause
If the checker still needs human input, src/websocket/listener/turn.ts hands the stop to src/websocket/listener/turn-approval.ts. That code path emits a control request over the websocket, waits for the response, and keeps the approval state inside the turn lifecycle so a resumed stream picks up the same conversation state.
Pre-hooks
runPreToolUseHooks() runs at the start of executeTool() in src/tools/manager.ts, after approval has resolved and before the tool body runs. A pre-hook can still stop the call early, which keeps policy and environment checks in one place instead of pushing them into each tool. For setup details, see the official hooks docs
.
Sandboxed execution
The harness wraps the launched process only after the earlier gates clear. src/permissions/sandbox-gate.ts and src/permissions/sandbox-policy.ts decide whether a sandboxed launch can run and which posture it gets, then src/sandbox/seatbelt.ts or src/sandbox/bwrap.ts adds the backend-specific wrapper around the launcher. When the host cannot support a backend, the code warns and continues with the normal guard layer.
Post-hooks
Post-tool-use hooks run after execution finishes. They cannot block or replace the tool’s result, though their feedback is appended to it.
Result back to the engine
After hooks finish, the listener hands the final result back into the streaming turn and keeps the turn lifecycle in sync. That handoff lets the model continue with the tool outcome instead of with an abstract event, for the client-side execution path.
The permission model
The permission model combines a mode with a rule stack. src/permissions/startup.ts and src/permissions/cli.ts set the starting mode, src/permissions/loader.ts merges settings from user, project, and local scopes, src/permissions/session.ts holds transient allow, deny, ask, and always-ask rules, and src/mods/permission-registry.ts adds mod-provided permissions and tool policies. The listener keeps per-conversation mode state in src/websocket/listener/permission-mode.ts, and src/reminders/engine.ts injects a permission-mode reminder so the model sees the active posture during the turn.
The current mode matters because it can auto-allow broad tool classes or keep the harness in a stricter posture without changing the underlying rules. A surface without an interactive user still runs the same stack, and only a live approval transport can satisfy an ask decision. For configuration examples, see the official permissions docs
.
Sandboxing
The harness probes the host before it wraps a launcher. On macOS, src/sandbox/availability.ts looks for sandbox-exec; on Linux, it checks for bwrap and verifies that unprivileged user namespaces actually work; on other platforms it reports that no filesystem sandbox backend exists. Memory-subagent filesystem sandboxing defaults on through LETTA_FS_SANDBOX, while cross-agent shell sandboxing stays opt-in so ordinary shell workflows do not break by default.
The memory-subagent path and the shell path solve different problems. src/agent/subagents/sandbox.ts constrains a memory-subagent’s whole child process so its own writes stay inside the scoped memory surface, while src/tools/impl/shell-sandbox.ts constrains shells launched by tools such as Bash and Shell so subprocesses inherit the same cross-agent guard. src/permissions/sandbox-policy.ts sets the writable roots and read-only roots for each posture, and src/sandbox/seatbelt.ts or src/sandbox/bwrap.ts turns that policy into backend-specific arguments for sandbox-exec or bwrap.
Sandboxing stays best effort, not magical. When the host cannot provide a backend, the harness logs a warning once and keeps running, because the code can only enforce what the environment supports. That keeps the guarantee honest: the client enforces the boundary when a kernel backend exists, and it falls back gracefully when it does not.
Related seams
Skills, subagents, and mods form the adjacent extension layer, and the turn lifecycle exposes them on every surface that streams a conversation. The app server and the SDK show where that client boundary meets the programmatic SDK, while Skills, subagents, and mods explains the neighboring extension model.
Where to look in the code
src/tools/manager.tsandsrc/tools/toolset.ts— assemble the tool catalog and mod extensions.src/hooks/index.tsandsrc/websocket/listener/turn-approval.ts— observe tool calls and pause for approval.src/permissions/checker.ts,src/permissions/mode.ts,src/permissions/loader.ts, andsrc/permissions/session.ts— decide whether a call can run.src/reminders/engine.tsandsrc/websocket/listener/permission-mode.ts— keep the active permission posture visible during a turn.src/permissions/sandbox-gate.ts,src/permissions/sandbox-policy.ts, andsrc/sandbox/availability.ts— choose whether the host can enter a sandboxed path.src/sandbox/seatbelt.ts,src/sandbox/bwrap.ts,src/agent/subagents/sandbox.ts, andsrc/tools/impl/shell-sandbox.ts— apply the backend-specific launch wrappers for subagents and shell tools.