Skip to Content
DocsToolsBuilt-in tools

Built-in tools

Four tools any Agent can register to gain the canonical “Claude-Code- shaped” capability set:

ToolWhat it does
read_tool()Read a text file with line numbers. Pageable for long files.
write_tool()Create or overwrite a text file.
edit_tool()Find-and-replace inside an existing file with uniqueness check.
bash_tool()Run a shell command with timeout + destructive-command denylist.

All four are factory functions: they take a workdir (and a few safety knobs for bash_tool) and return a ready-to-register Tool instance. The closure captures the workdir, so the resulting tool is workdir-scoped. The model can’t escape via ../ or absolute paths.

Quickstart

from loomflow import Agent from loomflow.tools import bash_tool, edit_tool, read_tool, write_tool agent = Agent( "You are a research agent.", model="claude-opus-4-7", tools=[ read_tool(workdir="/tmp/agent_work"), write_tool(workdir="/tmp/agent_work"), edit_tool(workdir="/tmp/agent_work"), bash_tool(workdir="/tmp/agent_work", timeout=30.0), ], )

Or as a bundle:

from loomflow.tools import bash_tool, filesystem_tools agent = Agent( "...", model="...", tools=filesystem_tools("/tmp/agent_work") + [bash_tool("/tmp/agent_work")], )

filesystem_tools(workdir) returns [read, write, edit]. The three filesystem tools, no bash.

Default workdir

Pass workdir=None (the default) and the framework lazily creates a shared temp directory under $TMPDIR/jeeves_agent_* once per process. All built-in tool factories called without an explicit workdir share that same directory:

from loomflow import Agent from loomflow.tools import bash_tool, default_workdir, read_tool agent = Agent( "...", model="...", tools=[read_tool(), bash_tool()], # both see the same tempdir ) print(default_workdir()) # /var/folders/.../jeeves_agent_xxxxx

The directory is NOT auto-cleaned at process exit. Leave that to the OS’s tempdir cleanup so debug data survives a crash.

read_tool

Returns the file’s text with 1-indexed line numbers prefixed (one line per output line):

1 from typing import Any 2 3 def main() -> None: 4 ...

This format lets edit_tool work without ambiguity later. The model has the surrounding lines in scope.

read_tool( workdir=None, # None → default tempdir name="read", # the tool name the model sees line_limit=2000, # max lines per call )

The model passes path, optionally offset (default 0) and limit (default 2000). Long files are truncated and the tool returns a hint like "... (32 more line(s); call read() again with offset=2000)".

write_tool

Creates or overwrites a text file. Returns a confirmation with the byte count.

write_tool( workdir=None, name="write", create_parents=True, # auto-mkdir parents )

Marked destructive=True. Overwrites can lose data, so StandardPermissions(mode=Mode.DEFAULT) will route through the approval handler.

edit_tool

Find-and-replace inside an existing file. Behaviour matches Claude Code’s Edit tool exactly:

  • old_string must be EXACTLY present (whitespace, indentation, line breaks). Mismatch → error.
  • old_string must appear EXACTLY once in the file unless replace_all=True is passed. Forces the model to give enough surrounding context for unambiguous matches.
  • new_string replaces the matched region (or every occurrence if replace_all=True).
edit_tool(workdir=None, name="edit")

Marked destructive=True.

bash_tool

Runs a shell command via /bin/sh -c. Output is structured:

$ pytest -q [exit=0] --- stdout --- ..... 5 passed in 0.32s
bash_tool( workdir=None, name="bash", timeout=30.0, # seconds; subprocess killed on timeout allow_pattern=None, # callable; OVERRIDES default deny list extra_env=None, # dict merged into subprocess env )

Default deny list

The default deny list rejects clearly-dangerous commands:

PatternBlocks
rm -rf / (root only)Recursive root delete
\bsudo\bPrivilege escalation
\bmkfs\bFormat a filesystem
dd if=.*of=/dev/Write to raw device
Fork bomb (:(){ :|:& };:)Resource exhaustion
> /dev/sdWrite to raw disk
chmod 777 /Permissions on root

The denylist is conservative, rm -rf /tmp/foo (a normal cleanup) is NOT flagged because rm -rf / only matches when the slash is followed by whitespace or end-of-line.

Overriding with allow_pattern

When you pass allow_pattern=callable, the default deny list is disabled entirely. Your callable is the only filter. You take full responsibility:

import re ALLOW = re.compile(r"^(ls|cat|grep|head|tail|find|wc|date) ") bash_tool(allow_pattern=lambda cmd: bool(ALLOW.match(cmd)))

Output truncation

stdout and stderr are truncated to 10,000 characters each, with a ... (truncated, N more bytes) hint. Long-running tasks shouldn’t flood the model’s context.

Path safety

All four tools use the same path resolver: any argument that resolves outside the workdir (via absolute path or ../ traversal) raises PathEscapeError, caught and returned as "ERROR: ..." to the model. The model sees it as a tool result and can adjust.

For symlink-aware path validation across an arbitrary tool host, wrap with FilesystemSandbox.

Why Claude-Code-shaped? The four tools’ interface mirrors Claude Code’s: 1-indexed line numbers in reads, exact-match edits with uniqueness, structured bash output. This is on purpose. It’s the shape that produces the highest-quality outputs from current frontier models, and it’s the foundation of the upcoming Deep Agent architecture (planner + filesystem state + subagent registry).

Last updated on