5. Tools
Tools are what turn a chat model into an agent. The model asks for one, plank runs it, feeds the result back, and the model continues โ repeating until a generation asks for nothing more. That whole loop is one turn.
You do not invoke tools directly; you ask for an outcome and the model chooses. What you control is the policy: the sandbox, plan mode, which extensions are installed, and how much of the machinery you see.
The built-in set
Files
| Tool | What it does |
|---|---|
read |
read a file (or a slice of one) |
more |
continue reading where the last read stopped |
write |
create a file, or overwrite one |
list |
list a directory |
glob |
find files by name pattern across a tree โ ** crosses
directories, * stays within one component |
edit |
replace a region of a file, anchored on surrounding text |
search |
search file contents |
When the sandbox is on, write and edit are
held to the same boundary as the shell: a path outside the workspace
(the working directory, the system temp directories, any configured
writablePaths, and ~/.plank only while you
have granted that) is refused with
Tool error: write path escapes workspace: <path>.
Reads are not contained.
edit and an overwriting write render as
diff cards in the TUI: an Update(path)
header, an added/removed summary, and red/green @@ hunks. A
genuinely new file streams its contents dimmed as it is written, so you
can watch it appear.
Shell
| Tool | What it does |
|---|---|
bash |
run a shell command |
bash_status |
poll a running job's output |
bash_stop |
stop a running job |
Bash commands are tracked jobs, not blocking one-shot calls: each owns a process, reader threads, and an output file, so the model can start a long build, do something else, and check back. The first observation is head-biased so headers and early errors are visible; later ones are tail-biased so you see recent output rather than the top of a log.
Every job runs in its own process group, and so do the commands you
type with !. Stopping a job or hitting its timeout kills
the whole tree, so a sleep 600; echo ok or a
cmd | tee pipeline cannot outlive plank. A job that ran
past its timeout is reaped by whatever tool call comes next, not only
when the model polls it. Interrupting a running command (Ctrl-C in the
REPL, Esc in the TUI) kills its group and reports exit status 143.
Documents
read on a .pdf transparently converts the
file to Markdown first, so a PDF is just a readable file:
summarize the first few pages of Claude-Code-Manuale-Completo.pdf
Conversion uses spatial text extraction, with OCR filling in pages
that have no text layer โ so both born-digital and scanned PDFs work.
The result is cached in ~/.plank/doc-cache/, keyed by
content, so a second read of the same document costs nothing.
Everything else behaves normally: paging, line numbers, and
more continuation all work, and the model only ever sees
your path, never the cache path. A file that cannot be parsed
comes back as a plain tool error.
PDF is currently the only converted format.
view_image hands the model an actual picture instead of
a path. It is served unconditionally, backed by the vision encoder plank
keeps beside the main model, and it works on the usual formats:
what does the error in screenshot.png say?
The image is encoded locally within a 384 visual token budget, so a
large screenshot is scaled down before the model sees it. Pasting an
image into the prompt is the same road: your message carries the cached
path, and the model calls view_image on it. See The interface.
Web
| Tool | What it does |
|---|---|
google_search |
search the web |
visit_page |
fetch and read a page |
Web access asks for your consent the first time, and you can grant it standing so it stops asking.
The pattern that works is to say what you want done with the results, not just what to look up:
search the web for the latest news about <topic> and summarize
The model searches, opens the pages worth opening, and reports. If it answers from memory when you wanted current information, say so โ "it's a different one, find it online" โ and it will go look.
Agent machinery
| Tool | What it does |
|---|---|
task |
maintain a task list that survives compaction |
agent |
delegate a bounded sub-task to a fresh subagent |
skill |
invoke an installed skill by name |
ask |
ask you a multiple-choice question |
EnterPlanMode / ExitPlanMode |
enter and leave read-only plan mode |
EnterWorktree / ExitWorktree |
work in an isolated checkout of the repository |
mcp_describe |
fetch the full schema of a non-primary MCP tool |
mcp_list_resources /
mcp_read_resource |
list and read MCP resources |
mcp__<server>__<tool> |
any tool from a connected MCP server |
The bash sandbox
On macOS, model-initiated shell commands run under
/usr/bin/sandbox-exec with a generated Seatbelt profile:
read everywhere, write only under the working directory, temp
directories, and any roots you allow. It is on by default,
because a model-chosen command should not be able to write outside the
project it was pointed at.
Commands you type with ! or !! are
never sandboxed โ you typing the command is the authorization.
Configure it in ~/.plank/sandbox.json, overlaid by
./.plank/sandbox.json:
{
"enabled": true,
"writablePaths": ["/some/extra/root"],
"excludedCommands": ["git push*", "brew *"]
}
The project file can only tighten the policy. Its
"enabled": true is honoured, but
"enabled": false, writablePaths and
excludedCommands in ./.plank/sandbox.json are
ignored, since a cloned repository must not be able to widen the sandbox
of whoever opens it; anything that relaxes the sandbox goes in
~/.plank/sandbox.json. excludedCommands
glob-matches the whole command line and skips the sandbox for it โ a
convenience escape hatch, not a security boundary.
The same roots contain the write and edit
tools (see Files), so a file the shell could not
write, the model cannot write by another route either.
Turn it off for a run with --no-sandbox, or permanently
with safety.sandbox: false.
Plan mode
When a task is risky or ambiguous, the model can call
EnterPlanMode. While it is active, every workspace-mutating
tool is refused โ write, edit,
bash โ and only read-only tools work, so the model
researches and designs without touching anything.
It leaves by calling ExitPlanMode with a proposed plan,
which you approve or reject. On approval the gate lifts and it may edit;
on rejection plan mode stays on and it refines the plan.
You can also just ask for it: "plan this out before you touch anything."
Worktrees
A worktree is a second checkout of the same repository, on its own branch, in its own directory. Ask for one and the model moves into it: every tool's working directory switches, so whatever it edits cannot touch the files you have open.
in a worktree called refactor-parser, try splitting the parser into two modules
Worktrees live under .plank/worktrees/<name> in
the repository root, on a branch called
worktree-<name>. The model only reaches for one when
you say so โ for ordinary branch work it runs git through
bash, which is what you want.
When the work is done it leaves, either keeping the worktree (it stays on disk with its branch, for you to review, merge, or come back to) or removing it. A removal that would destroy uncommitted files or commits that exist nowhere else is refused, and the refusal says what would be lost:
Tool error: worktree 'refactor-parser' has unsaved work, so it was not removed.
2 commits not present on the base branch
1 uncommitted file(s): src/parse.rs
The model can override that, but it has to do so explicitly. If plank cannot verify the state at all โ git fails, say โ it refuses too, on the principle that not knowing is not the same as knowing there is nothing there.
To start a whole session in a worktree instead, pass
--worktree:
plank --worktree refactor-parser
plank --worktree-pr 412 # based on pull request #412
That form goes further than the tool: the worktree becomes the session's project, so the hooks, subagent definitions, and project settings that apply are the ones found there.
Two settings tune it, both under worktree in
settings.json: sparsePaths narrows a new
worktree to a few directories of a large repository, and
symlinkDirectories links heavy build directories
(target, node_modules) from the main checkout
instead of duplicating them. A .worktreeinclude file in the
repository root lists gitignored files โ a .env, a local
build config โ to copy into each new worktree; only files that are
both listed there and actually gitignored are copied.
Questions
When a turn is genuinely ambiguous, the model can call
ask instead of guessing: a multiple-choice question with a
short header and two to seven options, shown as a panel in the TUI or a
numbered list in the REPL. It blocks until you answer, and degrades
cleanly when there is nobody to ask (headless mode).
ask.maxOptions caps how many options one question may
offer (default 7; the minimum of 2 is fixed).
Task lists
The task tool keeps a plan the model can add to and
update โ statuses are pending, in_progress,
and done. The list is model-visible and survives
compaction, which is the point: a long job does not lose its
plan when the conversation is summarized. /tasks shows it,
and the status bar carries a task counter.
Subagents
The agent tool hands a self-contained sub-task to a
fresh subagent working in its own scoped context, and returns
only its final report to the main conversation. This is
how a long research detour stays out of your transcript.
Delegation is bounded: a subagent cannot itself delegate. You can
also start one yourself with /subagent <task> (or
/subagent:<name> <task>), and define named
subagents with their own instructions โ see Extending plank.
Watching (or not watching) tools run
By default the UI stays clean: tool-call banners and tool output are hidden, and the status bar just names what is running. Turn them on when you want to see the mechanics:
/config ui.showToolCalls true
/config ui.showToolResults true
Neither changes what the model receives โ the tools run and the results are fed back either way.
Tool calls inside thinking
By default plank dispatches tool calls the model emits inside its
thinking block. Set engine.thinkingToolCalls to
false for strict parity with the ds4 reference, where such
a call is instead recovered forward by force-closing the thinking
block.
Next: Sessions โ