# Tools
The model works through six built-in tools. The descriptions below are sent to the model exactly as written (a test checks this).
| Tool | Asks by default | Description |
|---|---|---|
read |
No | Read a UTF-8 text file. Returns its lines numbered from 1, like cat -n. For long files, pass offset (first line, 1-based) and limit (number of lines). |
write |
Yes | Create or overwrite a file with the given content, creating parent directories. |
edit |
Yes | Replace exact text in a file. old_string must match the file exactly, including whitespace, and only once unless replace_all is true. |
grep |
No | Search file contents with a regular expression (Rust regex syntax). Skips .git and files ignored by .gitignore. Returns matching lines as path:line: text. |
ls |
No | List a directory, marking directories with a trailing /. With glob, list every file under path that matches it instead, skipping .git and ignored files. |
bash |
Yes | Run a command with bash in the working directory. Returns the exit status and output (stdout, then stderr). Standard input is empty. To leave a process running, redirect its output: cmd > log 2>&1 &. |
| Tool | Argument | Type | Required | Default | Description |
|---|---|---|---|---|---|
all but bash |
path |
string | for read, write, edit |
. |
Path, relative to the working directory or absolute. |
read |
offset |
integer ≥ 1 | 1 | First line to return, 1-based. | |
read |
limit |
integer ≥ 1 | 2000 | How many lines to return. | |
write |
content |
string | yes | The complete new contents of the file. | |
edit |
old_string |
string | yes | The text to replace, copied exactly from the file. | |
edit |
new_string |
string | yes | The text to put in its place. | |
edit |
replace_all |
boolean | false | Replace every occurrence. | |
grep |
pattern |
string | yes | The regular expression. | |
grep, ls |
glob |
string | Only files whose path matches, e.g. *.rs or src/**/*.ts. |
||
grep |
ignore_case |
boolean | false | Match regardless of case. | |
bash |
command |
string | yes | The command line to run. | |
bash |
timeout_secs |
integer 1–600 | 120 | Seconds before the command is killed. |
The stock runtime, basic.lug, adds two that ask nothing, so the model can read the Lug it
runs in before changing your config or a plugin: lua_docs searches the Lua reference
(lug.docs: the annotations, the Lua API and the built-in modules), and
lug_plugins lists your init.lua and each plugin's directory, whose source it then reads
with read and grep.
Tools run unsandboxed for now, with your authority and lug's environment, in the
project directory. Approvals are the guard: by default write, edit and bash ask
before they run.
## Approvals
The approval option decides which calls ask first:
| Value | Behaviour |
|---|---|
off |
Nothing asks. Every call runs, except those a rule denies. |
policy (default) |
Rules, tools allowed always, and each tool's default above decide |
all |
Every call asks, except those a rule denies and tools allowed always |
The dialog shows what the call will do, with the diff for an edit or write computed
from the file on disk. Its keys are in keys. Pressing A
allows that tool for the rest of the session, including calls to it already waiting;
:approvals lists those tools and
:approvals clear revokes them. Decisions are logged to
~/.local/state/lug/approvals.log.
The calls the model makes in one turn run at once, up to tool_concurrency (10 by
default; see configuration). Those that ask wait in the dialog
one at a time, and each runs as soon as it is allowed. write and edit change one file
at a time, so two edits to a file both land. The model gets the results in the order it
made the calls.
### Auto mode
Auto mode is approval = "off": every call runs without asking, unless a rule denies
it. Start in it with lug --auto (or lug.opt.approval = "off" in init.lua),
and toggle it with :auto or <S-Tab>; turning it off restores the mode it replaced.
The status line shows AUTO while it is on. Since tools are not sandboxed yet, the
model can then change or delete anything you can, with nothing asking first.
Rules decide from Lua, for one tool or, with "*", for every tool:
lug.tool.rule("bash", function(args)
if args.command:match("^git status") then return "allow" end
return "ask"
end)
lug.tool.rule("write", "deny")
A rule returns "allow", "ask", "deny" or nothing, and perhaps a reason. Each call
runs the rules for its tool and for "*" in the order they were made; the first "deny"
denies it, even in auto mode. Otherwise any "ask" asks, then any rule that failed, and
any "allow" allows. lug.tool.rule returns a function that removes the rule, and nil
in place of a rule removes the tool's rules. A rule function runs as a coroutine and may
wait, on lug.ui.confirm say: the call waits for its answer.
A plugin can answer in the dialog's place with lug.session.approve(fn), which gets the
call and its preview and returns what the dialog's keys would (see
the Lua API).
## Limits
| Limit | Value |
|---|---|
| Output, every tool | 30,000 bytes; bash keeps the end, the others the start |
read lines, when limit is not given |
2,000 |
Longest line (read, grep) |
2,000 characters |
bash and lug.proc.run timeout |
120 s; bash allows up to 600 s, and lug.proc.run takes a timeout |
bash runs /bin/bash -c (or /bin/sh -c) with empty stdin, in its own process group,
which is killed on timeout and on cancel. A background process whose output is
redirected (cmd > log 2>&1 &) outlives the call and is killed when lug exits.
lug.proc.run and lug.proc.spawn start programs the same way.
## Tools from Lua
lug.tool.register("today", {
description = "Return today's date.",
ask = false,
handler = function()
return lug.proc.run({ "date", "+%F" }).stdout
end,
})
namemust match^[A-Za-z0-9_-]{1,64}$and not be taken.parametersis a JSON schema as a table, withtype = "object"; it defaults to no arguments. Every call is checked against it, and a schema using keywords Lug does not check is refused (see the Lua API).output, a JSON schema, makes the result a table that is checked too.model = falsekeeps the tool for Lua alone, throughlug.tool.call.ask(defaulttrue) says whether calls ask first inpolicymode. Set it tofalseonly for a tool that changes nothing.preview(args)returns what the approval dialog shows about a call:{ path?, created?, old?, new?, text?, notes? }, a diff fromoldtonewor elsetext. It must answer at once; without it, or if it fails, the dialog shows the arguments.- The handler gets the arguments as a table, and a
ctxtable saying who called, and runs as a coroutine, so it may wait. A string is the result, another value is sent as JSON, andnil, messageor an error is a failed call. After three errors the tool is disabled. - A tool registered during a run is offered from the next run.
The handler runs inside lug with your authority, so text the model controls should
reach commands only through lug.proc.run's argument list, never a shell string.