Note: agents were used extensively to develop shfs.
shfs (shell filesystem) is a simulated fish shell (subset) environment for executing filesystem-related commands.
Live demo: shfs.lawsonoates.com
shfs is inspired by Bun's $ shell api and provides a pluggable filesystem interface allowing custom storage.
shfs is designed to be used by agents needing a filesystem without having to spin up a sandbox.
- Why fish? it's simple.
- Why a subset of fish? shfs is only for simulating a filesystem, only a subset is really needed.
The subset includes deterministic script features and filesystem commands needed for agent workflows. More details are available in the Subset Boundary, Grammar, and Lexer Specification.
bun add shfsimport { Shell } from "shfs";
import { MemoryFS } from "shfs/fs";
const fs = new MemoryFS();
fs.setFile("hello.txt", "hello world");
const { $ } = new Shell(fs);
const content = await $`cat hello.txt`.text();
console.log(content);The shell runs over the FS interface exported from shfs/fs. MemoryFS is the built-in implementation; custom backends implement the same interface. See docs/filesystem-interface.md.
shfs is fish-inspired but intentionally not a full fish shell. It targets deterministic behavior over a virtual filesystem.
Included behavior:
- fish list variables with indexing and slices (
$var,$var[1],$var[2..-1],count) - variable assignment (
set,set -g/-l, erase-e, query-q, append/prepend-a/-p, slice assignment) - command-scoped assignments (
name=value command) - command substitution (
(cmd)and$(cmd), including$(cmd)inside double quotes) - multi-statement scripts with newline and
; - boolean chaining, combiners, and negation (
and,or,&&,||,not,!,$status) - control flow and blocks (
if/else if/else,switch/case,while,for ... in,begin ... end,break,continue) - functions (
function name [-a names]...end,$argv,return) - script-core builtins (
test/[,echo,read,string,true,false,count) - core path behavior (
cd,pwd,.,.., absolute/relative paths) - fish-style wildcard expansion (
*,?,[ ... ],**) - symlink creation, traversal, and command semantics (
find -H/-L/-P,-type l,-xtype, symlink-preserving recursivecp) - stable, deterministic error contracts
Explicitly out of scope:
CDPATH-based directory lookup- host OS/process emulation, job control, and interactive shell UX
- full fish compatibility or fish-verbatim error text
Canonical boundary doc: notes/shfs-subset-boundary.md.
- variables are lists:
set name a b cstores three elements;echo $name[2]printsb- slices support ranges, negative indices, and open ends (
$name[2..-1],$name[..2]) set -gpersists across runs,set -lis block-local, unscopedsetkeeps the existing scopeset -e nameerases,set -q namequeries,set -a/-pappend/prepend$statusexposes the last command status;$argvholds function arguments- unquoted list expansion yields one argument per element; quoted expansion joins with spaces; empty lists elide the word
- quoted PATH-like variables use colon-delimited rendering with fish empty-entry semantics
- command substitution:
(echo subdir)and$(echo subdir)can be used as arguments$(cmd)also works inside double quotes and preserves inner newlines- unquoted substitutions split output lines into arguments; output can be sliced (
(cmd)[2]) - explicit fields from
string splitandstring split0remain distinct, including trailing empty fields
- script statements:
- newline and semicolon statement separators
and/orkeywords and&&/||combiners chain on the previous statusnot/!negate a job's statusif/else if/else,switch/case,while,for ... in, andbegin ... endblocks withbreak/continuefunction name [-a names] ... enddefines functions with$argvandreturnname=value commandscopes an assignment to a single command
- quoting and expansion:
- unquoted words support fish-style character, byte, Unicode, octal, and line-continuation escapes
- quoted wildcard text is treated literally
- unquoted wildcard text is expanded for in-scope path arguments
Filesystem/path commands:
- cat
- cd
- cp
- find
- grep
- head
- ls
- mkdir
- mv
- pwd
- rm
- sort
- tail
- touch
- tree
- wc
- xargs
Script builtins:
- count
- echo
- false
- read
- set
- string
- test (and its
[alias) - true
Notable builtin coverage:
echosupports fish-style-n,-s,-e, and-Eoption parsing and escape decodingstring matchandstring replacesupport literal and regex modes;string split0preserves explicit NUL-delimited fieldscat,head, andtailpreserve exact file and stdin bytes when no text transformation is requested
Symlink-related command behavior:
findsupports the-H,-L, and-Plink-following modes, plus the-type land-xtypepredicatescp -rpreserves symlinks rather than following them- there is no
lncommand; create symlinks via the filesystem interface
For in-scope path-taking commands, unquoted wildcard patterns use fish-style expansion:
- supported pattern families:
*,?,[ ... ],** - hidden files only match when explicitly requested (for example
.*) - trailing slash forms match directories (for example
*/) - quoted wildcard characters are literal text and are not expanded
- unmatched wildcard patterns fail with deterministic errors (
<command>: no matches found: <pattern>) - commands expecting one post-expansion path reject multi-match expansions with deterministic cardinality errors
shfs is designed to be a tool used by agents to enable the benefits of a filesystem like progressive disclosure.
Agent Tool Prompt is a prompt that can be used as a tool description for an agent.
program ::= separator* statement (separator+ statement)* separator*
separator ::= ";" | NEWLINE | COMMENT
statement ::= chain_prefix? negation? (pipeline | block)
chain_prefix ::= "and" | "or"
negation ::= "not" | "!"
pipeline ::= command ("|" NEWLINE* command)*
block ::= if_block | while_block | for_block | begin_block | switch_block | function_block
switch_block ::= "switch" word separator+ case_clause* "end"
case_clause ::= "case" word* separator+ statement*
command ::= word command_part*
command_part ::= word | redirection
redirection ::= "<" word | ">" word | ">>" word
word ::= word_part+
word_part ::= literal | glob | substitution
substitution ::= "(" program ")" | "$(" program ")"
literal ::= bare_text | single_quoted | double_quoted
single_quoted ::= "'" single_quoted_text "'"
double_quoted ::= '"' double_quoted_part* '"'
double_quoted_part ::= double_quoted_text | substitution
glob ::= "*" | "?" | bracket_glob | "**"Notes:
- words can mix literal text, glob parts, and command substitution in one token (for example
foo(echo bar)baz) $varand$statusare documented runtime expansion forms; they are preserved through parsing and expanded during execution
MIT