Capability tools: filesystem & shell

The Signals & Sorcery agent surface includes a small set of capability tools that reach outside the music app: they read and write files on your machine and run external commands like ffmpeg. They're how the in-app chat-plugin agent fulfils requests like:

  • "What files are in my ~/Music folder?"
  • "Make a recording checklist at ~/Desktop/checklist.md with these items."
  • "Convert this WAV to MP3 with ffmpeg."
  • "Install ffmpeg."

Every capability tool is gated by a per-call consent dialog. The agent never accesses your filesystem or runs a shell command without you explicitly approving each operation in a modal.

When the agent calls a capability tool, you see a native dialog showing:

  • What the agent wants to do ("Read directory", "Write file", "Run shell command").
  • A short reason the agent provided.
  • The exact details: full path, args list, byte counts, timeout, cwd. Args are shown as a list (["-i", "in.wav", "out.mp3"]), never concatenated into a shell string.

You click Allow or Cancel. Cancel returns a structured failure to the agent; it can ask you a different way or move on.

No persistent allowlist in v1. Every call prompts every time. We may add an "always allow" toggle later based on real-world friction; for now, simplicity wins.

Tools

fs_list_directory

List a directory's contents.

sas fs list --path ~/Music
sas run fs_list_directory -p path=~/Documents -p depth=2
InputNotes
pathRequired. ~ and ~/ expand to your home directory.
depthOptional integer 1–3, default 1.
maxEntriesOptional cap, default 1000.

Returns { resolvedPath, entries: [{ name, type, size?, modifiedMs? }], truncated }.

fs_read_file

Read a text file.

sas fs read --path ~/Desktop/notes.md
sas run fs_read_file -p path=~/notes.md -p maxBytes=1000000
InputNotes
pathRequired. Tilde-expanded.
maxBytesOptional cap, default 1 MB. Files larger than this are rejected without prompting.

Returns { resolvedPath, size, content }.

Recursive name/glob search.

sas fs search --root-path ~/Music --name-pattern "*.wav"
sas run fs_search -p rootPath=~/Documents -p namePattern=todo
InputNotes
rootPathRequired. Tilde-expanded.
namePatternRequired. Supports * and ? globs; otherwise case-insensitive substring.
extensionsOptional ["wav","mp3"] allowlist.
maxResultsOptional cap, default 100.

Returns { resolvedRoot, matches: [{ path, size, modifiedMs }], truncated }.

fs_write_file

Create, overwrite, or append a text file. Atomic write: produces a .tmp sibling then renames.

sas fs write --path ~/Desktop/todo.md --content "..."
sas run fs_write_file -p path=~/notes.txt -p content="hello" -p mode=append
InputNotes
pathRequired. Tilde-expanded. Parent directories created if missing.
contentRequired string. (For binary writes, use a different tool.)
modeOptional, "overwrite" (default) or "append".

If the target file exists, the consent dialog warns explicitly that the file will be overwritten and its current contents lost, with the existing file size shown.

Returns { resolvedPath, bytesWritten, mode, replacedSize? }.

shell_exec

Run an external command. Uses execFile-style invocation: args are passed positionally to the OS, never through a shell interpreter. This means there is no shell-injection vector: even if the agent tries to pass ; rm -rf / as an arg, it lands as a literal arg to the named command, which almost certainly errors out.

sas run shell_exec --json '{"command":"ffmpeg","args":["-version"]}'
sas run shell_exec --json '{"command":"brew","args":["install","ffmpeg"]}'
InputNotes
commandRequired. Executable name (on PATH) or absolute path.
argsOptional string[]. No shell parsing.
cwdOptional working directory. Tilde-expanded.
timeoutMsDefault 30 s; max 10 min. Process killed (SIGKILL) on timeout.
maxOutputBytesDefault 1 MB; stdout/stderr beyond this is truncated and a truncated: true flag returned.

Returns { command, args, cwd, exitCode, stdout, stderr, durationMs, truncated }.

A non-zero exit code is reported with success: true; the agent has the exit code and decides whether to retry. Only timeouts and spawn failures return success: false.

Built-in safety: the deny list

A short pre-consent denylist refuses obviously-malicious commands before any dialog fires:

  • rm -rf / or rm -rf ~
  • dd of=/dev/sda (and any raw block device)
  • mkfs* (any filesystem reformat tool)
  • The classic fork bomb (:(){ :|:& };:)

If you ever need to run one of these legitimately, you'll have to do it outside the chat-plugin.

Tools the v1 surface deliberately omits

Not includedWhy
fs_deleteNo good undo path without a trash-folder mechanism. The agent doesn't get to delete files in v1.
package_install (wrapper)Already covered by shell_exec("brew", ["install", "X"]). Keeping a single consent surface is cleaner than two parallel paths.
Persistent "always allow" allowlistv1 design choice: always-prompt. Will revisit based on real friction.

For a read:

┌────────────────────────────────────────────────────────────┐
│  Read directory                                            │
│                                                            │
│  The chat-plugin agent wants to list the contents of a     │
│  directory on your machine.                                │
│                                                            │
│    path: /Users/you/Music                                  │
│    depth: 1                                                │
│    maxEntries: 1000                                        │
│                                                            │
│              [ Cancel ]   [ Allow ]   ← default            │
└────────────────────────────────────────────────────────────┘

For a shell call, the dialog uses warning styling and Cancel is the default button; accidental Enter denies:

┌────────────────────────────────────────────────────────────┐
│  ⚠  Run shell command                                       │
│                                                            │
│  The chat-plugin agent wants to run an external command on │
│  your machine.                                             │
│                                                            │
│    command: ffmpeg                                         │
│    args: ["-i","input.wav","output.mp3"]                   │
│    cwd: (current process cwd)                              │
│    timeoutMs: 30000                                        │
│                                                            │
│              [ Cancel ]  ← default     [ Allow ]           │
└────────────────────────────────────────────────────────────┘

Discovery from your agent

The capability tools are part of the default /api/v1/actions curated surface. They're project-scoped, so the chat-plugin's scene-default view (?scope=scene) doesn't list them by default; the chat-plugin internally queries without a scope filter, so it sees them too.

# Confirm they're registered
sas list-actions | grep -E 'fs_|shell_exec'

If you're writing your own agent integration, just call these like any other action. The consent dialog will pop on the user's machine; your agent receives the structured result when they decide.

Last Updated:
Contributors: shiehn