Skip to Content
ToolsBuilt-in tools

Built-in tools

Loomflow ships ready-made tools for reading, writing and searching files and running shell commands, modelled on the tools Claude Code uses. Register them when you want an agent that can work in a directory (a coding agent, say) without writing those tools yourself. There are seven:

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.
grep_tool()Search file contents by regex within the workdir.
find_tool()Find files by glob pattern within the workdir.
ls_tool()List directory contents within the workdir.

Two helpers go with them: filesystem_tools() returns all of the file tools at once (everything except bash_tool), and default_workdir() gives you a default directory to scope them to. For web search there is also web_tool(), which registers a web_search tool (DuckDuckGo by default, or Serper with an API key).

The tool factories 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, grep, find, ls] — the six 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 filesystem tools use the same path resolver: any argument that resolves outside the workdir (via absolute path, ../ traversal or a symlink) raises PathEscapeError, caught and returned as "ERROR: ..." to the model. The model sees it as a tool result and can adjust. grep also skips files in the tree that are symlinks to somewhere outside the workdir, as read refuses them. (Before 0.17, grep followed them.)

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

Why Claude-Code-shaped? The tools’ interface mirrors Claude Code’s: 1-indexed line numbers in reads, exact-match edits with uniqueness, structured bash output. This is the shape that produces high-quality outputs from current frontier models.

Last updated on