Default Tools - Filesystem
July 22, 2026 · View on GitHub
description: Default Tools - Filesystem reference. 18 safety.fs-wrapped tools (list-roots · read · list · stat · grep · search · count · slice · sort · cut · find · write · append · edit · copy · move · delete · deleteDir) over readable roots and a writable working directory.
Default Tools - Filesystem
The 18 tools in default-tool-specs-builtin-fs.json are the safety.fs surface as ready-to-call tools - a shell-style filesystem pipeline covering list-roots, read, list, stat, grep, recursive content search, count, slice, sort, cut, find, write, append, targeted edit, copy, move, and delete (file and directory). They operate within two boundaries:
- Readable roots - directories the tools may read from, recursively. The user's home directory is a readable root by default.
- A working directory (
spring.ai.playground.tool-studio.fs.base-path, default${user.home}/spring-ai-playground/workspace) - the only writable location, and where relative paths resolve. When a tool runs inside a chat, writes are confined to a per-conversation subdirectory of it (<workspace>/<conversationId>/), created on first write and removed when you delete the conversation;listAllowedDirectoriesreports that exact path at runtime. This holds however the chat reaches the tool - a fixed selection travels through the built-in MCP server's loopback connection, which carries the conversation identity along with the call. A caller with no conversation - an external MCP client invoking the same exposed tools - writes to the working directory root instead.
So a relative path resolves under the working directory; an absolute path may be read anywhere under a readable root, but writes are confined to the working directory. Call listAllowedDirectories first to learn the exact absolute paths before reading or writing.
Three capability tiers. Read tools (listDir, readTextFile, grepFile, searchInFiles, ...) need only fileRead and rank L3. Write tools that create or amend a file (writeTextFile, appendTextFile, editTextFile, copyFile) need fileWrite and rank L4. Tools that remove or relocate data (moveFile, deleteFile, deleteDir) carry a destructive flag and rank L5 - deliberately above a plain overwrite, because the loss is not recoverable from the file itself. Every write and destructive tool is gated by human-in-the-loop approval, so its risk chip shows two levels with an arrow (L5 → L4, L4 → L3): the level before the arrow is the inherent posture that still drives sandbox permissions and the audit log, and the level after is the effective posture once a mandatory approval is credited (one band lower). A poisoned tool description forfeits that credit, and the runtime denies the call outright if approval is unavailable, so a credited mitigation can never silently no-op.
Because they ride on java.nio.file.Path / Files, separator handling (/ vs \), case folding, and symlink semantics are normalised at the JVM layer - these tools behave identically on macOS, Windows, and Linux. See Tool Studio: Cross-platform by design for the mechanics, and Tool Studio: Filesystem mode for the read-only / read-write sandbox split.
The 18 filesystem tools { #the-filesystem-tools }
More detail
Reports which directories the filesystem tools may touch: the readable roots (your home directory and the app's spring-ai-playground folder - anything under them can be read) and the single working directory for this chat (the only writable location, where relative paths resolve). Files the user uploads are saved under the working directory's uploads/ subfolder, so after calling this you can listDir the working directory or its uploads/ folder to see the files already there, then readTextFile one to re-process it. Call this first to learn the absolute paths. Takes no arguments. Uses safety.fs.readRoots() / workspace().
Parameters
(none - takes no arguments)
Sandbox - Sandbox needs fileRead (L3). Reports the boundaries only; it reads no file contents.
JS source
/**
* Reports the filesystem boundaries these tools operate within.
*
* - readRoots — absolute roots that may be READ (recursively).
* - workingDirectory — the single writable dir; relative paths resolve here.
*
* Uses host helpers: safety.fs.readRoots, safety.fs.workspace.
*/
return {
readRoots: Array.from(safety.fs.readRoots()),
workingDirectory: safety.fs.workspace(),
};
More detail
A relative path resolves under the working directory; an absolute path is allowed anywhere under a readable root (the user's home directory by default). Call listAllowedDirectories first to learn the working directory and readable roots. Uses safety.fs.readText().
Parameters
| Param | Type | Req | Description |
|---|---|---|---|
path | STRING | ✓ | Path to read - relative (under the working directory) or absolute (under a readable root) |
Sandbox - Sandbox needs fileRead (L3). A relative path resolves under the working directory; an absolute path may sit anywhere under a readable root (${user.home} by default). Call listAllowedDirectories to see both.
JS source
/**
* Reads a UTF-8 text file inside the playground's FS base directory.
*
* Path is resolved RELATIVE to the base path (which is set via
* `spring.ai.playground.tool-studio.fs.base-path`,
* defaulting to the user's home directory). Anything attempting
* to escape the base (e.g. `../`) is rejected by safety.fs.
*
* Uses host helper: safety.fs.readText.
*/
if (path == null || path === '') throw new Error('path required');
return safety.fs.readText(path);
Parameters
| Param | Type | Req | Description |
|---|---|---|---|
dir | STRING | Directory to list - relative (default '.', under the working directory) or absolute (under a readable root) |
Sandbox - Sandbox needs fileRead (L3). A relative path resolves under the working directory; an absolute path may sit anywhere under a readable root (${user.home} by default). Call listAllowedDirectories to see both.
JS source
/**
* Lists immediate entries (files + directories) of a directory.
*
* Returned as an array of leaf names - no recursion, no full paths.
* Use `findFiles` for recursive globbing.
*
* Uses host helper: safety.fs.list.
*/
const target = (dir == null || dir === '') ? '.' : dir;
return safety.fs.list(target);
Parameters
| Param | Type | Req | Description |
|---|---|---|---|
path | STRING | ✓ | Path to read - relative (under the working directory) or absolute (under a readable root) |
Sandbox - Sandbox needs fileRead (L3). A relative path resolves under the working directory; an absolute path may sit anywhere under a readable root (${user.home} by default). Call listAllowedDirectories to see both.
JS source
/**
* Returns { size, mtime, directory } for a path inside the FS base.
*
* - size - file size in bytes (0 for directories).
* - mtime - ISO timestamp of last modification.
* - directory - true if the path is a directory.
*
* Uses host helper: safety.fs.stat.
*/
if (path == null || path === '') throw new Error('path required');
return safety.fs.stat(path);
Parameters
| Param | Type | Req | Description |
|---|---|---|---|
path | STRING | ✓ | Path to the file - relative (under the working directory) or absolute (under a readable root) |
Sandbox - Sandbox needs fileRead (L3). A relative path resolves under the working directory; an absolute path may sit anywhere under a readable root (${user.home} by default). Call listAllowedDirectories to see both.
JS source
/**
* Counts lines in a UTF-8 text file inside the FS base.
*
* Uses host helper: safety.fs.lineCount.
*/
if (path == null || path === '') throw new Error('path required');
return safety.fs.lineCount(path);
Parameters
| Param | Type | Req | Description |
|---|---|---|---|
path | STRING | ✓ | Path to the file - relative (under the working directory) or absolute (under a readable root) |
start | INTEGER | First line index (0-based inclusive; negatives from end) | |
end | INTEGER | End line index (0-based exclusive; negatives from end) |
Sandbox - Sandbox needs fileRead (L3). A relative path resolves under the working directory; an absolute path may sit anywhere under a readable root (${user.home} by default). Call listAllowedDirectories to see both.
JS source
/**
* Returns lines [start, end) from a UTF-8 text file (Python-slice semantics).
*
* - `start` is 0-based and inclusive.
* - `end` is 0-based and exclusive.
* - Negative values count from the end: -1 == size - 1.
* - Missing `start` / `end` defaults to the file's full range.
*
* Uses host helper: safety.fs.slice.
*/
if (path == null || path === '') throw new Error('path required');
return safety.fs.slice(path, start, end);
Parameters
| Param | Type | Req | Description |
|---|---|---|---|
path | STRING | ✓ | Path to the file - relative (under the working directory) or absolute (under a readable root) |
reverse | BOOLEAN | Sort descending | |
numeric | BOOLEAN | Sort lines numerically | |
caseInsensitive | BOOLEAN | Ignore case when comparing | |
unique | BOOLEAN | Drop duplicate lines |
Sandbox - Sandbox needs fileRead (L3). A relative path resolves under the working directory; an absolute path may sit anywhere under a readable root (${user.home} by default). Call listAllowedDirectories to see both.
JS source
/**
* Sorts a file's lines and returns them as an array.
*
* Options:
* reverse - descending order
* numeric - numeric comparison (otherwise lexical)
* caseInsensitive - compare lowercased
* unique - drop duplicates
*
* Uses host helper: safety.fs.sort.
*/
if (path == null || path === '') throw new Error('path required');
return safety.fs.sort(path, {
reverse: !!reverse,
numeric: !!numeric,
caseInsensitive: !!caseInsensitive,
unique: !!unique,
});
Parameters
| Param | Type | Req | Description |
|---|---|---|---|
pattern | STRING | ✓ | Regex pattern (JavaScript flavour) |
path | STRING | ✓ | Path to the file - relative (under the working directory) or absolute (under a readable root) |
caseInsensitive | BOOLEAN | Match case-insensitively | |
numbered | BOOLEAN | Prefix each result with 'N:' (1-based line number) | |
limit | INTEGER | Max matches to return (0 = no limit) |
Sandbox - Sandbox needs fileRead (L3). A relative path resolves under the working directory; an absolute path may sit anywhere under a readable root (${user.home} by default). Call listAllowedDirectories to see both.
JS source
/**
* Greps a file's lines against a regex and returns the hits.
*
* Result is an array of matching lines. With `numbered=true` each entry
* is prefixed by its 1-based line number, e.g. "42: TODO fix this".
*
* Uses host helper: safety.fs.grep.
*/
if (pattern == null || pattern === '') throw new Error('pattern required');
if (path == null || path === '') throw new Error('path required');
return safety.fs.grep(pattern, path, {
caseInsensitive: !!caseInsensitive,
numbered: !!numbered,
limit: Number.isInteger(limit) ? limit : 0,
});
Parameters
| Param | Type | Req | Description |
|---|---|---|---|
dir | STRING | Directory to search from - relative (default '.') or absolute (under a readable root) | |
glob | STRING | Glob pattern (default '*') | |
maxDepth | INTEGER | Max recursion depth (0 = unlimited) | |
type | STRING | 'file' | 'dir' | omit for both |
Sandbox - Sandbox needs fileRead (L3). A relative path resolves under the working directory; an absolute path may sit anywhere under a readable root (${user.home} by default). Call listAllowedDirectories to see both.
JS source
/**
* Recursively finds entries matching a glob inside a directory.
*
* - Glob accepts `*` and `?` wildcards (POSIX glob, NOT regex).
* - maxDepth limits recursion; 0 means unlimited.
* - type='file' / 'dir' filters; omit to return both.
*
* Uses host helper: safety.fs.find.
*/
const target = (dir == null || dir === '') ? '.' : dir;
const pattern = (glob == null || glob === '') ? '*' : glob;
return safety.fs.find(target, pattern, {
maxDepth: Number.isInteger(maxDepth) ? maxDepth : 0,
type: (type === 'file' || type === 'dir') ? type : null,
});
Parameters
| Param | Type | Req | Description |
|---|---|---|---|
path | STRING | ✓ | Path to the file - relative (under the working directory) or absolute (under a readable root) |
fields | ARRAY | ✓ | Array of 1-based field indices to keep, e.g. [1, 3] |
delimiter | STRING | Field delimiter character or regex (default '\t' tab) | |
regex | BOOLEAN | Treat delimiter as a regex pattern instead of literal |
Sandbox - Sandbox needs fileRead (L3). A relative path resolves under the working directory; an absolute path may sit anywhere under a readable root (${user.home} by default). Call listAllowedDirectories to see both.
JS source
/**
* Extracts selected fields from each line of a delimited file.
*
* - `fields` - 1-based field indices to keep (array). [1, 3] picks columns 1 and 3.
* - `delimiter` - single character (literal) OR a regex pattern when `regex=true`.
* Defaults to tab (\t).
*
* Uses host helper: safety.fs.cut. Each returned row is a delimiter-joined string.
*/
if (path == null || path === '') throw new Error('path required');
if (fields == null) throw new Error('fields required');
const fieldArr = [];
for (const f of fields) fieldArr.push(Number(f));
return safety.fs.cut(path, {
fields: fieldArr,
delimiter: (typeof delimiter === 'string' && delimiter.length > 0) ? delimiter : null,
regex: !!regex,
});
Parameters
| Param | Type | Req | Description |
|---|---|---|---|
path | STRING | ✓ | Path to write, inside the working directory |
content | STRING | ✓ | Full text content to write (UTF-8) |
Sandbox - Sandbox needs fileWrite (L4). spring.ai.playground.tool-studio.fs.base-path (default ${user.home}/spring-ai-playground/workspace) is the workspace root; writes from a chat are confined to a per-conversation subdirectory under it, and the returned path is the absolute location actually written. The helper auto-creates parent directories.
JS source
/**
* Writes a UTF-8 text file inside the workspace directory.
*
* - Overwrites any existing file at `path`.
* - Path is resolved RELATIVE to the workspace; escape attempts are rejected.
* - This tool needs the `fileWrite` sandbox permission (set on the spec).
*
* Uses host helper: safety.fs.writeText.
*/
if (path == null || path === '') throw new Error('path required');
if (content == null) throw new Error('content required');
const writtenPath = safety.fs.writeText(path, String(content));
return { ok: true, path: writtenPath, bytes: new TextEncoder().encode(String(content)).length };
More detail
Answers "which files mention X" or "where is Y used" in one call. Binary / non-UTF-8 files are skipped, and limit caps the total number of hits. A relative dir resolves under the working directory; an absolute dir must sit under a readable root. Uses safety.fs.searchInFiles().
Parameters
| Param | Type | Req | Description |
|---|---|---|---|
pattern | STRING | ✓ | Regex pattern (JavaScript flavour) matched against each line |
dir | STRING | Directory to search from (default '.', under the working directory; absolute under a readable root) | |
glob | STRING | Filename glob limiting which files are scanned (default '*') | |
caseInsensitive | BOOLEAN | Match case-insensitively | |
maxDepth | INTEGER | Max recursion depth (0 = unlimited) | |
limit | INTEGER | Max total matches to return (0 = default cap) |
Sandbox - Sandbox needs fileRead (L3). Read-only, no approval required. A relative path resolves under the working directory; an absolute path may sit anywhere under a readable root.
JS source
if (pattern == null || pattern === '') throw new Error('pattern required');
const target = (dir == null || dir === '') ? '.' : dir;
const g = (glob == null || glob === '') ? '*' : glob;
return safety.fs.searchInFiles(target, g, pattern, {
caseInsensitive: !!caseInsensitive,
maxDepth: Number.isInteger(maxDepth) ? maxDepth : 0,
limit: Number.isInteger(limit) ? limit : 0,
});
Parameters
| Param | Type | Req | Description |
|---|---|---|---|
path | STRING | ✓ | File to append to, inside the working directory |
content | STRING | ✓ | UTF-8 text appended verbatim after the existing content |
Sandbox - Sandbox needs fileWrite (inherent L4). Gated by human-in-the-loop approval, so the effective chip reads L4 → L3. Writes are confined to the working directory; escape attempts are rejected.
JS source
if (path == null || path === '') throw new Error('path required');
if (content == null) throw new Error('content required');
const writtenPath = safety.fs.appendText(path, String(content));
return { ok: true, path: writtenPath, bytesAppended: new TextEncoder().encode(String(content)).length };
More detail
Use this instead of writeTextFile whenever you only need to change part of a file. Include enough surrounding context to make oldString unique, or set replaceAll=true. Returns { ok, path, replacements }. Uses safety.fs.editText().
Parameters
| Param | Type | Req | Description |
|---|---|---|---|
path | STRING | ✓ | File to edit, inside the working directory |
oldString | STRING | ✓ | Exact text to find (verbatim). Must be unique unless replaceAll is true |
newString | STRING | ✓ | Replacement text (empty string deletes the match) |
replaceAll | BOOLEAN | Replace every occurrence instead of requiring a single unique match |
Sandbox - Sandbox needs fileWrite (inherent L4). Gated by human-in-the-loop approval, so the effective chip reads L4 → L3. Reads then rewrites the target in place; confined to the working directory.
JS source
if (path == null || path === '') throw new Error('path required');
if (oldString == null || oldString === '') throw new Error('oldString required');
const result = safety.fs.editText(path, String(oldString), newString == null ? '' : String(newString), !!replaceAll);
return { ok: true, path: result.path, replacements: result.replacements };
More detail
Parent directories are created automatically and an existing destination is overwritten. Useful for pulling a home-directory or uploaded file into the writable workspace before processing it. Uses safety.fs.copy().
Parameters
| Param | Type | Req | Description |
|---|---|---|---|
from | STRING | ✓ | Source file - relative (working directory) or absolute (under a readable root) |
to | STRING | ✓ | Destination path, inside the working directory |
Sandbox - Sandbox needs fileWrite (inherent L4). Gated by human-in-the-loop approval, so the effective chip reads L4 → L3. The source is read from any readable root; only the destination write is confined to the working directory.
JS source
if (from == null || from === '') throw new Error('from required');
if (to == null || to === '') throw new Error('to required');
const path = safety.fs.copy(String(from), String(to));
return { ok: true, path };
Parameters
| Param | Type | Req | Description |
|---|---|---|---|
from | STRING | ✓ | Source file, inside the working directory |
to | STRING | ✓ | Destination path (new name), inside the working directory |
Sandbox - Sandbox needs fileWrite and is flagged destructive (inherent L5). Gated by human-in-the-loop approval, so the effective chip reads L5 → L4. Both source and destination are confined to the working directory.
JS source
if (from == null || from === '') throw new Error('from required');
if (to == null || to === '') throw new Error('to required');
const path = safety.fs.move(String(from), String(to));
return { ok: true, path };
Parameters
| Param | Type | Req | Description |
|---|---|---|---|
path | STRING | ✓ | File to delete, inside the working directory |
Sandbox - Sandbox needs fileWrite and is flagged destructive (inherent L5). Gated by human-in-the-loop approval, so the effective chip reads L5 → L4. Returns { ok, deleted, path } - deleted is false when the file did not exist.
JS source
if (path == null || path === '') throw new Error('path required');
const deleted = safety.fs.delete(String(path));
return { ok: true, deleted, path: String(path) };
Parameters
| Param | Type | Req | Description |
|---|---|---|---|
path | STRING | ✓ | Directory to delete recursively, inside the working directory (cannot be the workspace root) |
Sandbox - Sandbox needs fileWrite and is flagged destructive (inherent L5). Gated by human-in-the-loop approval, so the effective chip reads L5 → L4. Returns { ok, removed, path } - removed is the number of entries deleted.
JS source
if (path == null || path === '') throw new Error('path required');
const removed = safety.fs.deleteDir(String(path));
return { ok: true, removed, path: String(path) };
Composition patterns (shell-style filesystem chains)
These tools mirror the standard Unix-shell pipeline shape, but every step is a JSON-returning function so the agent can reason between calls:
- Read → filter → trim → save -
listDir(dir)→grepFile(pattern, path)→sliceFile(path, start, end)→writeTextFile(outPath, content). The canonical "summarise recent errors from a log directory" flow. - Find → cut → ETL -
findFiles(dir, glob='*.csv')→ loop withcutFileFields(path, fields=[1,3])to project a directory of CSVs into one structured dataset. - Sort dedupe → count -
sortFile(path, numeric=true, unique=true)→lineCount(path)to deduplicate a numeric stream in place and report the resulting size. - Stat-first guard -
statFile(path)→ branch onsize/lastModified→ only run the rest of the pipeline if the file changed since the last run. - Search → edit -
searchInFiles(dir, glob, pattern)finds every hit across the tree, theneditTextFile(path, oldString, newString)applies a targeted, unique-match change to each file - the code-refactor loop. - Copy → transform → clean up -
copyFile(upload, 'work.csv')pulls an upload into the writable workspace; process it, thendeleteFile('work.csv')ordeleteDir('tmp')removes the scratch copy. Every write and destructive step pauses for approval first.
Tutorial 8: Default Tool Recipes walks the Read → filter → trim → save chain end-to-end as summariseRecentLogs.
Keys & secrets
One configuration value, no real secrets.
| Variable | What it does | Default | Where to set |
|---|---|---|---|
SPRING_AI_PLAYGROUND_TOOL_STUDIO_FS_BASE_PATH | The safety.fs working directory - the only writable location, and where relative paths resolve. Reads also reach the readable roots (the home directory by default). | ${user.home}/spring-ai-playground/workspace | Launcher Environment Variables card, or export SPRING_AI_PLAYGROUND_TOOL_STUDIO_FS_BASE_PATH=/path before launch |
The File Toolkit preset opts every read tool (now including searchInFiles) into fileRead automatically. The write tools (writeTextFile, appendTextFile, editTextFile, copyFile) require fileWrite (L4), and the destructive tools (moveFile, deleteFile, deleteDir) additionally carry the destructive flag (L5); you enable these per-tool in the Sandbox & Capabilities pane - see Tool Studio: Filesystem mode. In Agentic Chat, the Workspace organizer preset wires the full set - survey, restructure, clean up - with every mutating call gated by human-in-the-loop approval.
→ Tool Studio: Filesystem mode - fileRead / fileWrite semantics and base-path enforcement.