How Mako Runtime Implements Bash, Read, and Write Built-In Tools

Mako’s runtime registers Bash, Read, and Write as MakaTool objects via the buildBuiltinTools factory in packages/runtime/src/builtin-tools.ts, where each tool executes within sandboxed filesystem and shell boundaries enforced by the active execution context.

The Apache Mako project provides a runtime environment for LLM-driven agents that interact with the filesystem and shell through strictly defined built-in tools. Understanding how Mako implements Bash, Read, and Write at the runtime level reveals the architecture behind its secure, sandboxed execution model. This article examines the source code in apache/maka to explain the factory pattern, schema validation, and security boundaries that govern these core utilities.

Tool Registration Architecture

The runtime initializes its built-in toolkit through the buildBuiltinTools function located at [lines 94-122 of packages/runtime/src/builtin-tools.ts](https://github.com/apache/maka/blob/main/packages/runtime/src/builtin-tools.ts#L94-L122). This factory creates and returns an array of MakaTool objects, always including the three core file-system tools—Read, Write, and Edit—alongside the Bash command tool.

import { buildBuiltinTools } from '@maka/runtime';

const tools = buildBuiltinTools({
  permissionProfile: myPermissionProfile,
});

The function wires in auxiliary services—including the workspace executor, sandbox manager, and permission profile—ensuring every tool invocation runs inside a controlled execution boundary. Each tool conforms to the MakaTool interface, which describes its metadata, Zod-based input schema, and asynchronous implementation.

Read Tool Implementation

The Read tool, defined at lines 350-449, serves a dual purpose: reading text files from the sandboxed filesystem or retrieving whole runtime resources referenced by a ref identifier.

Input Schema and Validation

The tool accepts a Zod schema that enforces mutual exclusivity between a path property (with optional offset and limit parameters) and a ref property. The schema is pre-processed to ensure only one input type is provided, then exposed to the provider as a JSON schema.

Execution Logic

When invoked, the implementation branches based on the input type:

  • Resource References: If a ref is supplied, the tool resolves the reference via runtimeResources or attachmentResources.
  • Filesystem Paths: If a path is supplied, the request forwards to the sandboxed filesystem worker through filesystem.execute({ kind: 'read', … }).

The result can be a plain text payload, an image snapshot handled via snapshotImage, or an error wrapped by internalFilesystemReadFailure.

await tools.find(t => t.name === 'Read')!.impl({
  path: 'docs/README.md',
  offset: 0,
  limit: 20,
}, {
  cwd: '/home/user/project',
  sessionId: 'sess-123',
  abortSignal: new AbortController().signal,
});

Write Tool Implementation

The Write tool, found at lines 450-473, persists string content to a specified file path within the sandboxed environment.

Schema and Operation

The tool uses a simple Zod object schema requiring { path: string, content: string }. The implementation calls the filesystem worker with operation: { kind: 'write', path, content } via the shared filesystem.execute helper at lines 635-645.

Result Handling

On success, the tool returns either a file diff (kind: 'file_diff') or a plain write acknowledgment (kind: 'file_write'). Errors are captured and wrapped by internalFilesystemWriteFailure to ensure consistent error reporting to the LLM.

await tools.find(t => t.name === 'Write')!.impl({
  path: 'tmp/out.txt',
  content: 'Hello, Mako!',
}, {
  cwd: '/home/user/project',
  sessionId: 'sess-123',
  abortSignal: new AbortController().signal,
});

Bash Tool Implementation

The Bash tool provides arbitrary shell command execution within the session’s current working directory. Its construction and execution logic spans lines 494-511 for tool creation and lines 558-750 for the executor implementation.

Tool Construction Strategy

The runtime selects between two construction strategies:

  • Managed Bash: Built via buildManagedBashTool when a custom ShellRunLauncher is supplied.
  • Simple Executor: Built via buildExecutorBashTool for direct executor-driven execution.

Schema and Validation

The tool schema combines bashBoundaryIntentSchema and sandboxBoundaryExpansionSchema to express sandbox-boundary intent and optional timeout parameters.

Execution Flow

Before execution, the command undergoes validation via throwIfShellSetupFailed and potential transformation by the sandbox manager through sandboxCommand. The runtime then delegates execution to executor.exec, which spawns the command using the detected defaultShellPlan. Results are wrapped by shapeTerminalResult and returned as a TerminalResult object.

await tools.find(t => t.name === 'Bash')!.impl({
  command: 'ls -l *.ts',
  timeout_ms: 30_000,
}, {
  cwd: '/home/user/project',
  sessionId: 'sess-123',
  abortSignal: new AbortController().signal,
  emitOutput: (output) => console.log(output),
});

Sandbox Boundaries and Security

All three tools operate within the active execution boundary (ctx.executionBoundary) enforced by the filesystem.execute method. For Bash commands, additional sandbox transformations occur via sandboxCommand, which may rewrite commands, adjust environment variables, or reject execution if the permission profile demands an unavailable sandbox configuration. This architecture ensures that filesystem and shell operations remain confined to the permitted workspace scope.

Model Output Conversion

Both Read and Write expose a toModelOutput hook that maps raw tool results into LLM-compatible formats. The Bash tool utilizes conversion logic in [bash-model-output.ts](https://github.com/apache/maka/blob/main/packages/runtime/src/bash-model-output.ts), while file operations rely on [file-tool-model-output.ts](https://github.com/apache/maka/blob/main/packages/runtime/src/file-tool-model-output.ts). These hooks transform internal results—such as TerminalResult or file diff objects—into deterministic, JSON-compatible structures via functions like bashToolResultToModelOutput and fileWriteToolResultToModelOutput.

Summary

  • Central Registry: Mako defines all built-in tools in packages/runtime/src/builtin-tools.ts via the buildBuiltinTools factory (lines 94-122), which returns MakaTool objects for Read, Write, and Bash.
  • Schema Enforcement: Each tool uses Zod schemas to validate inputs, with Read supporting mutually exclusive path or ref parameters, while Write requires path and content.
  • Sandboxed Execution: Filesystem operations route through filesystem.execute (lines 635-645) with active permission profiles, while Bash commands undergo sandboxCommand transformation before executor.exec spawns them.
  • Result Normalization: Tools convert raw results to LLM-friendly formats via toModelOutput hooks located in dedicated model-output modules.

Frequently Asked Questions

How does the Mako runtime register its built-in tools?

The runtime calls the buildBuiltinTools factory function in packages/runtime/src/builtin-tools.ts (lines 94-122), which instantiates MakaTool objects for Read, Write, Edit, and Bash. This factory injects required services like the sandbox manager and permission profile to ensure controlled execution boundaries.

What security mechanisms protect Bash tool execution?

The Bash tool implements multiple security layers: input validation via throwIfShellSetupFailed, command transformation through sandboxCommand, and execution confinement within the ctx.executionBoundary. The tool also checks against the active permission profile and may reject commands requiring unsupported sandbox features.

How does the Read tool differentiate between filesystem paths and runtime resources?

The Read tool uses a Zod schema enforced at lines 350-449 that accepts either a path string or a ref identifier, but never both simultaneously. When ref is provided, the tool queries runtimeResources or attachmentResources; when path is provided, it delegates to the sandboxed filesystem.execute method.

What determines the output format returned to the LLM model?

The output format is determined by the toModelOutput hook implemented on each tool. Read and Write use converters in file-tool-model-output.ts, while Bash uses bash-model-output.ts. These hooks transform internal results into deterministic, JSON-compatible structures via functions like bashToolResultToModelOutput.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →