
Go-native, cross-platform tools for common local tasks, plus a small registry that makes them easy to expose to LLM tool-calling systems.
Table of contents
Features at a glance
- Cross-platform implementations for Linux, macOS, and Windows.
- Instance-owned tools with configurable path policy.
- Deterministic text-editing tools for line-oriented workflows.
- Shell command and script execution with timeouts, output caps, and command blocking.
- Structured tool manifests and normalized outputs for text, files, and images.
Grouped under fstool.
-
readfile
encoding=text: reads UTF-8 text and supports extracted text for PDFs.
encoding=binary: returns base64, emitting image outputs for image/* MIME types and file outputs otherwise.
- Safety: size caps and symlink hardening.
-
writefile
encoding=text: write UTF-8 content.
encoding=binary: write base64-decoded bytes.
- Options:
overwrite, createParents (bounded), atomic writes, size caps, symlink hardening.
-
deletefile
- Safe delete by moving to trash.
trashDir=auto tries system trash when possible and otherwise falls back to a local .trash directory.
-
searchfiles: recursively search file paths and UTF-8 text content using RE2 regex.
-
listdirectory: list entries under a directory, optionally filtered by glob.
-
statpath: inspect a path.
-
mimeforpath: best-effort MIME type detection.
-
mimeforextension: MIME lookup for an extension.
Grouped under exectool.
-
shellcommand
- Execute one or more shell commands via a selected shell.
- Supports session-like persistence for
workDir and env across calls.
- Sessions are not persistent shell processes.
-
runscript
- Run an existing script from disk with arguments and environment overrides.
- Uses extension-based interpreter selection via
RunScriptPolicy.
Text processing tools
Grouped under texttool.
readtextrange: read lines, optionally constrained by unique start/end marker blocks.
findtext: find matches with context.
inserttextlines: insert lines at start/end or relative to a uniquely matched anchor block.
replacetextlines: replace exact line blocks, with optional disambiguation using adjacent lines.
deletetextlines: delete exact line blocks, with optional disambiguation using adjacent lines.
Grouped under imagetool.
readimage: read intrinsic metadata and optionally include base64 content.
Package overview
llmtools: registry and tool registration helpers.
spec: tool manifests and output union types.
fstool: filesystem tools.
exectool: shell command execution and script execution.
texttool: safe, deterministic line-based text editing tools.
imagetool: image tools.
Registry
The registry provides:
- tool registration and lookup by
spec.FuncID
- stable manifest ordering (
Tools() sorted by slug + funcID)
- per-registry default call timeout via
WithDefaultCallTimeout
- per-call timeout override via
llmtools.WithCallTimeout(...)
- panic-to-error recovery around tool execution
Registry.Call returns []spec.ToolOutputUnion.
- Most tools are registered via
RegisterTypedAsTextTool, which wraps the Go result as JSON and returns it as a single text output item.
- Tools that naturally return binary or image data can emit typed
image or file outputs.
Path policy
All tools are instance-owned. Hosts can configure:
workBaseDir: base directory for resolving relative paths
allowedRoots: optional allowlist roots; when set, resolved tool paths must stay within these roots
blockSymlinks: optional symlink hardening for supported operations
This is path policy, not a general OS sandbox for child processes.
Examples
All examples are provided as end-to-end integration tests that:
- start from a registry
- register tools scoped to a temp directory
- execute realistic read/modify loops, text edits, shell sessions, script execution, and binary/image workflows
Examples:
- Text read/modify loop:
text test
- Filesystem, MIME, safe delete, binary, and image flows:
fs + image test
- Shell sessions, environment persistence, and
runscript: exec test
Quick model
For exectool, the most useful mental model is:
workBaseDir chooses the default base for relative paths.
shellcommand.workDir chooses the actual child-process working directory.
shellcommand.env and runscript.env add per-call environment overrides.
- Shell sessions persist tool-managed
workDir and env, not shell process state.
- Commands are executed in fresh child processes, not in a long-lived interactive terminal.
Shell selection and bootstrap defaults
shellcommand supports explicit shell selection with:
auto
bash, zsh, sh, dash, ksh, fish
pwsh, powershell, cmd
ExecTool also supports tool-level defaults:
WithDefaultShell(...)
WithBaseEnv(...)
If the caller does not provide them, NewExecTool(...) best-effort bootstraps defaults:
- detects a preferred shell
- captures a narrow, execution-oriented base environment
Auto shell detection order is:
- Unix/macOS:
$SHELL
- account login shell from the host
- fallback by platform/tool availability
- Windows:
pwsh
powershell
cmd
You can also call BootstrapDefaults(ctx) directly and pass the results yourself.
ctx := context.Background()
defs, _ := exectool.BootstrapDefaults(ctx)
tool, err := exectool.NewExecTool(
exectool.WithWorkBaseDir(projectDir),
exectool.WithDefaultShell(defs.DefaultShell),
exectool.WithBaseEnv(defs.BaseEnv),
)
Working directory behavior
shellcommand resolves working directory in this order:
args.workDir
- session workdir
- tool
workBaseDir
Key points:
- If
workDir is omitted and there is no session workdir, execution starts in the tool workBaseDir.
- If
workBaseDir is omitted, the path policy falls back to the current process working directory unless allowedRoots is set, in which case it defaults to the first canonical allowed root.
- Relative paths are resolved against
workBaseDir unless a more specific absolute/derived path is supplied by the tool logic.
For stable desktop-app behavior, hosts should usually:
- set
workBaseDir explicitly
- pass an absolute per-call
workDir for the active project/workspace
Environment behavior
Effective environment merge order is:
-
shellcommand
- current process environment
- tool base env (
WithBaseEnv or bootstrapped defaults)
- session env
- per-call
env
-
runscript
- current process environment
- tool base env (
WithBaseEnv or bootstrapped defaults)
- per-call
env
Bootstrapped base env is intentionally filtered. It keeps variables that are usually relevant to command and interpreter resolution, for example:
- command lookup:
PATH, PATHEXT
- home/temp/runtime basics:
HOME, USERPROFILE, TMP, TEMP, TMPDIR, SHELL, COMSPEC, app-data dirs
- locale basics:
LANG, LC_*, TERM
- common toolchain/version-manager variables:
ASDF_*, PYENV_*, NVM_*, VOLTA_*, SDKMAN_*, GOPATH, GOBIN, JAVA_HOME, PNPM_HOME, CARGO_HOME, CONDA_*
This avoids importing unrelated prompt/theme variables while still making command lookup and common toolchains behave more like the user’s shell.
Shell sessions: what persists and what does not
shellcommand sessions are persistent tool state, not persistent shell processes.
What persists across calls when you reuse sessionID:
workDir passed as a tool argument
env passed as a tool argument
What does not persist:
cd ... issued inside a command string
export FOO=... issued inside a command string
- aliases, functions, shell options, sourced files
- any in-memory shell state
So this works predictably:
{
"workDir": "/repo",
"commands": ["go test ./..."]
}
But this is not persistent across separate command entries or later calls:
{
"commands": ["cd /repo", "go test ./..."]
}
Each command entry is executed in a fresh shell process.
Command lookup and login-shell behavior
Working directory and command lookup are separate concerns.
- Working directory controls where the child process starts.
- Command lookup for bare names like
golangci-lint depends on PATH and shell behavior.
Important nuance:
- Commands are executed using non-persistent shell invocations.
- You should not assume they behave exactly like an interactive terminal.
- On Unix,
zsh -c/bash -c behavior differs from an interactive login terminal.
- On PowerShell, execution uses explicit non-interactive flags.
That is why BootstrapDefaults(...) exists: it captures a stable, tool-useful environment once, instead of relying on every command to run as an interactive login shell.
For predictable command execution, hosts should prefer:
- explicit
workDir
- explicit or bootstrapped base env
- explicit
shell or WithDefaultShell(...)
- absolute executable paths when needed
Run Script behavior
runscript:
- executes a pre-existing script from disk
- validates the script path through path policy
- selects how to invoke the script from
RunScriptPolicy
Default policy includes mappings such as:
.sh -> shell mode with sh
.bash -> shell mode with bash
.zsh -> shell mode with zsh
.ps1 -> direct PowerShell execution
.py -> interpreter mode with python3 on Unix and python on Windows
Behavioral notes:
runscript inherits tool base env and per-call env.
- Interpreter availability still depends on the effective environment, especially
PATH.
Execution limits and blocking
ExecutionPolicy controls:
- timeout
- output caps
- max commands
- max command length
There is also a non-overridable hard blocklist of command names and some heuristic blocking for obviously unsafe shell patterns.
The registry may also impose a call timeout. If you use both registry-level and tool-level timeouts, ensure the registry timeout is not shorter than the exec-tool timeout.
Important behavioral notes
executeParallel=true does not run commands concurrently. It means commands are treated as independent and execution does not stop on the first non-zero result.
shellcommand executes each command entry separately. If you need shell chaining semantics, put them in one command string, for example cd repo && go test ./....
workBaseDir is a default anchor, not a persistent current-directory concept by itself.
- If you omit both
workBaseDir and allowedRoots, relative/default behavior falls back to the host process working directory.
- If you set
allowedRoots but omit workBaseDir, the base defaults to the first canonical allowed root.
allowedRoots constrains tool-resolved paths; it does not turn child processes into a full OS sandbox.
- For packaged desktop apps, relying on ambient process cwd or ambient process env is usually less stable than passing explicit values.
Development
- Formatting follows
gofumpt and golines via golangci-lint. Rules are in .golangci.yml.
- Useful scripts are defined in
taskfile.yml; requires Task.
- Bug reports and PRs are welcome:
- Keep the public API (
package llmtools and spec) small and intentional.
- Avoid leaking provider-specific types through the public surface; put them under
internal/.
- Please run tests and linters before sending a PR.
License
Copyright (c) 2026 - Present - Pankaj Pipada
All source code in this repository, unless otherwise noted, is licensed under the MIT License.
See LICENSE for details.