npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@poe-platform/safe-bash

v0.1.743

Published

Extensible virtual shell with pluggable filesystems and streaming commands

Readme

safe-bash

Run shell scripts and command-line tools in your application against an explicit filesystem, without launching a host shell.

Import ffmpegCommands from @poe-platform/safe-bash/commands/ffmpeg and register it with shell.use(ffmpegCommands()) for in-memory media conversion and probing. Pass cloudflareWorkerLimits() explicitly to bound media work in Workers.

Quickstart

Install in a Node.js 22+ ESM application:

npm install @poe-platform/safe-bash
import { Shell, agentCommands, createMemoryFileSystem } from "@poe-platform/safe-bash";

const fs = createMemoryFileSystem();
const encoder = new TextEncoder();
await fs.mkdir("/work");
await fs.writeFile("/work/names.txt", encoder.encode("Ada\nGrace\nAda\n"));
await fs.writeFile("/work/run.sh", encoder.encode(`#!/bin/sh
set -eu
sort names.txt | uniq > names.sorted.txt
printf 'Hello, %s!\\n' "$1"
cat names.sorted.txt
`));

const shell = new Shell({ fs, cwd: "/work" }).use(agentCommands());
try {
  const result = await shell.exec("sh run.sh reader");
  if (result.exitCode !== 0) throw new Error(result.stderr);
  process.stdout.write(result.stdout);
} finally {
  await shell.dispose();
}

Output: Hello, reader!\nAda\nGrace\n. The script, input, and generated names.sorted.txt stay in memory. Results contain exitCode, stdout, stderr, stdoutBytes, and stderrBytes; use the byte fields for binary output. Each exec() starts fresh shell variables, functions, and working-directory state; filesystem changes persist in the supplied fs. The invocation-local umask starts at 0022, accepts octal or symbolic modes, and is inherited by child shells. Creation modes use the filesystem's capabilities; advisory modes do not enforce physical permissions, and the host process mask remains unchanged. Core and shell imports install a portable globalThis.Buffer when it is absent; an existing host Buffer is preserved. Workers do not need nodejs_compat for this.

Supported features and commands

Shell syntax

  • Quoting and escapes, variables and positional arguments (including ${@:offset:length} and ${*:offset:length} slices), parameter expansion (including scalar ${!name} indirection and ^/^^ and ,/,, case conversion with optional patterns), $(command) and backtick substitution, arithmetic expansion, and pathname globs.
  • Pipelines (|, |&), lists (;, &&, ||, !), file redirection (<, >, >>, <>), combined output redirection (&>, >& file, &>>), descriptor redirection such as 2>&1, here-documents, and here-strings. <> opens without truncation and requires a filesystem with descriptor support.
  • if/elif/else, case, for name in …, while, until, functions, groups { …; }, subshells ( … ) (including adjacent nested subshells), [[ … ]] (including file age/identity, special-file, mode and nameref predicates; literal/glob and ASCII ERE comparisons also accept UTF-8 locales, with ranges limited to C/POSIX collation, including C.UTF-8), arithmetic commands (( … )), and indexed arrays with arithmetic and relative negative element indices, including declare -a, local -a, and readonly -a array literals. Arithmetic array operands such as a[i] support reads, assignments, and increments in $(( … )), (( … )), for (( … )), and let, including associative keys. Array expansions support member slices, element substrings, member-wise trimming and substitution, and lazy element default, alternate, assignment, and error operators by default. Ownership predicates -O and -G in [[ … ]], test and [ use explicit capabilities: { predicateIdentity: { effectiveUid, effectiveGid } } on the shell or execution options; missing caller or filesystem identity is refused. File predicates in [[ … ]], test and [ evaluate symlink traversal loops as false; -L/-h still recognize the link itself. Access checks -r/-w/-x evaluate denied access as false, and -N compares modification and access times.
  • Virtual script files through sh, bash, or executable paths; source/. runs a script in the current shell. bash -n script.sh (also sh -n) checks syntax without executing commands; set -n / set -o noexec parses the remaining input without execution. set -e, set -u, and set -o pipefail control failures; set -a (or set -o allexport) exports subsequent variable assignments to child commands, and set +a disables automatic export. Bash-profile set -f / set -o noglob disables pathname expansion, and set +f restores it. set -C (or set -o noclobber) protects existing output files from > redirection; >| overrides it and >> still appends. shopt -s dotglob includes dotfiles in globs.

Shell builtins beyond the tools below: :, cd, pushd, popd, dirs, set, shift, export, local, declare, readonly, unset, read, getopts, let, shopt, umask, exit, return, break, continue, command, builtin, type, ., source, eval. pwd, true, and false also work without a command bundle. command -p bypasses functions and searches /bin:/usr/bin in the supplied filesystem while preserving PATH; registered commands remain available. Combine it with -v or -V for discovery, including command -pV printf. unset -v NAME removes variables; unset -f NAME removes functions without changing variables of the same name. Use -- to end option parsing. read -a NAME replaces an indexed array with the record's IFS-separated fields. The default read accepts -u 0 to select supplied stdin, plus -p PROMPT and -s for nonterminal input; prompts are suppressed and silent mode has no terminal effect. Other -u descriptors require the optional read extension. export -n NAME removes a variable's export attribute while keeping its value; export -f NAME passes a defined function to virtual child shells, and export -fn NAME stops passing it. export -p prints reusable variable declarations; export -fp lists exported functions. declare supports integer (-i), ASCII case conversion (-l/-u), scalar name references (-n), exports (-x), readonly (-r), arrays (-a/-A), global declarations (-g), inherited locals (-I), declaration printing (-p), and function inspection (-f/-F). typeset is an alias for declare. local accepts variable attribute flags and combinations such as -ai and -ar. declare, typeset, local, and export accept name+=value; integer variables add the arithmetic value instead of concatenating text. cd -L preserves symlinks in PWD (the default); cd -P resolves symlinks before following .. and records the physical directory. Use cd -- <path> for paths beginning with a dash. Each invocation starts with umask 0022; numeric and symbolic masks affect new files and default directory modes through the supplied filesystem. Host umask is unchanged and may further restrict real file modes; remote modes can be advisory. readonly -f name protects a function from redefinition and unset -f; readonly -f lists protected functions. unset -v removes variables independently. local -i count=2+3 evaluates scalar assignments as arithmetic; local -n ref=target reads and writes the named variable. Both attributes follow function scope and restore outer bindings on return. Indexed and associative arrays support readonly (-r), integer (-i, including arithmetic +=), and ASCII case conversion (-l/-u) on each element. Nameref targets must be simple variable names; combining nameref with integer or array attributes is unsupported.

Command bundle

agentCommands() registers all 79 commands below. They operate on the supplied filesystem and byte streams, not host executables. file recognizes bounded CSV, HTML, XML and shell/Python shebang text alongside JSON and binary headers, preserving the detected charset in MIME output; see its recognition limits.

hexdump and hd default to the BSD numeric and repeated-row profile. Use agentCommands({ hexdump: { dialect: "util-linux" } }) or hexdumpCommands({ dialect: "util-linux" }) for hexadecimal/octal counts, binary size suffixes such as 1KiB, decimal suffixes such as 1KB, and unsqueezed partial final rows. This selects those semantics; custom formats and other unsupported options remain unavailable.

| Purpose | Commands | | --- | --- | | Browse | pwd, ls (including -i/--inode for provider-reported inode numbers, with ? for unknown metadata; -Q/--quote-name and --indicator-style=none/slash/file-type/classify), tree, find (including -H to follow argument symlinks while keeping descendant links physical, and -D tree for virtual expression diagnostics; other debug modes are unsupported), du, file, basename, dirname, realpath (including lexical -s/--strip/--no-symlinks), readlink (including -m/--canonicalize-missing), which | | Files | mkdir, touch, cp, mv, rm, rmdir, ln, chmod, stat, mktemp. mkdir -m accepts octal and symbolic permission modes; symbolic modes start from 0777 masked by the current umask. rm --interactive=never removes without prompting; --interactive=always / -i reads a confirmation from stdin for each removal. --interactive=once / -I prompts once for recursive removal or more than three operands. cp -a / --archive copies directory trees without dereferencing symbolic links (the virtual archive profile is -RP; add preservation flags for metadata and hard links). cp -b / --backup[=simple\|numbered\|existing] preserves replaced files; -S / --suffix sets the simple backup suffix. cp -t DIR / --target-directory=DIR copies sources into a directory; -T / --no-target-directory treats the destination as a single path. mv -u / --update moves files only when the destination is absent or older; directories retain normal move behavior. mv -t DIR / --target-directory=DIR moves sources into an existing directory; -T / --no-target-directory treats the destination as an exact path. cp --remove-destination removes destination entries before copying, preserving other hard links and leaving destination symlink referents untouched. cp --preserve=mode retains permissions; -p retains modes and access/modification times and requires matching reported owners. -d copies symlinks and preserves links between copied entries. --attributes-only leaves existing bytes intact and creates empty missing files; combine it with preservation flags to update metadata. Unsupported metadata operations fail explicitly; ownership changes and symlink timestamps are unavailable. cp -v reports paths using the original operand spelling, including relative paths. ln -sr / --symbolic --relative computes a relative symlink target from the link directory, resolving existing symlinks and allowing missing sources. ln -v / --verbose reports each successful link as 'target' => 'source' (hard) or 'target' -> 'source' (symbolic). rmdir --ignore-fail-on-non-empty leaves nonempty directories untouched without reporting a failure; with -p, removal stops at the first nonempty parent. touch -h / --no-dereference accepts ordinary files and reads reference symlink metadata; symlink targets fail explicitly because symlink timestamp mutation is unavailable. touch -d / --date accepts epoch seconds (@0), ISO/RFC dates and the virtual date relative-date profile; -t [[CC]YY]MMDDhhmm[.ss] sets a calendar timestamp. | | Filter/search | cat, head, tail, wc, tee, cut, tr, sort, uniq, sed, awk, grep, rg, egrep, fgrep, fd. fd finds virtual files and directories with regex/glob patterns, extension/type/size/time filters, ignore files, and literal command execution; see fd usage. awk accepts --field-separator (-F), --source (-e), and --characters-as-bytes (-b); its strings and records are byte-oriented. cut -c selects raw bytes in C/POSIX locales (LC_ALL, then LC_CTYPE, then LANG); otherwise it selects UTF-8 characters, including when no locale is set. head and tail accept -z / --zero-terminated for NUL-delimited records. tail --follow[=descriptor\|name] follows retained files; --retry retries inaccessible files (-F combines name following and retry). -s / --sleep-interval sets polling seconds, and --max-unchanged-stats controls name reopening after unchanged polls; --max-idle=0 emits only the initial selection. Following requires retained-read filesystem support. uniq -D / --all-repeated[=none\|prepend\|separate] prints every repeated record; --group[=separate\|prepend\|append\|both] prints all records with group separators. wc -L / --max-line-length counts display columns with eight-column tab stops; UTF-8 widths use the frozen GNU/Linux C.UTF-8 profile. | | Format/combine | nl, seq, rev, tac, expand, unexpand, fold, fmt, strings, paste, comm, join, column, split, pr. pr --date-format=FORMAT customizes the UTC header date using the virtual date format directives. | | Structured text | jq, html-to-markdown, xq, xmllint. xq converts XML to JSON and applies jq filters. xmllint supports --xpath, --noout well-formedness checks, --format, and --c14n with comments; DTDs and schema validation are unsupported. XML modes and limits. jq supports ASCII escapes (-a), indentation (--indent, --tab), explicit color (-C / -M), NUL-delimited raw output (--raw-output0), and unbuffered writes. jq -S / --sort-keys sorts object keys recursively in JSON output; --stream / --stream-errors emit path events, and --seq handles JSON sequences within the same resource limits. --rawfile NAME FILE binds virtual-file text; --slurpfile NAME FILE binds its JSON values as an array. --args and --jsonargs expose string or JSON arguments through $ARGS.positional. jq -L DIRECTORY loads virtual .jq modules with zero-argument definitions; see jq options and limits. | | Bytes/checksums | base64, base32, xxd, od, md5sum, sha1sum, sha256sum, cksum. od -a prints ASCII character names, -f prints IEEE single-precision floats, and -i / -l print signed four/eight-byte integers. Explicit float types are -tf4 and -tf8. od --strings[=MIN] / -S[MIN] prints NUL-terminated strings with at least MIN characters (default 3). | | Archives | Tar extraction to the filesystem requires atomic backend confinement; MemoryFileSystem supports it, while backends without it refuse extraction. tar -xO remains available. gzip, gunzip, zcat, tar, zip, unzip. ZIP inspection supports unzip -Z -1 ARCHIVE [FILES...] for names only and unzip -z ARCHIVE for the archive comment, without extracting members. Other ZipInfo formats are unsupported. Tar supports gzip (-z), bzip2 (-j), xz (-J), suffix-selected creation (-a), and compression detection when reading. Creation accepts --mtime=@SECONDS or ISO/RFC dates, numeric --owner/--group, and octal --mode; --atime-preserve[=replace] restores source file and directory access times on timestamp-capable backends. Extraction accepts --touch/-m, --same-permissions/--preserve-permissions/-p, --no-same-permissions (virtual 022 mask), --no-same-owner, and directory restoration policies. Special permission bits remain stripped; ownership restoration (--same-owner), name lookup, symbolic modes and no-atime reads (--atime-preserve=system) are unsupported. --full-time shows UTC timestamps in verbose listings; --utc also enables verbose listing. --record-size=SIZE / -b N sets output record padding, -B accepts full-record reads, and --ignore-zeros / -i reads past zero blocks and concatenated archives. --seek / -n, --no-seek, and --force-local accept local VFS archives with sequential reads. --quoting-style=literal\|escape\|c controls displayed names; --totals reports archive byte counts on stderr. Extraction supports -k / --keep-old-files (preserve conflicts and return status 2), --skip-old-files (preserve silently), and --overwrite (default safe replacement); the last policy wins. Existing directories are merged. Path, symlink and input-archive safety checks apply to every policy. | | ZIP archives | zip, unzip. File extraction requires atomic ancestry verification; MemoryFileSystem supports it, while unsupported backends refuse extraction. unzip -n preserves existing regular files without prompting; -j flattens paths and skips directory entries. Selection uses original archive names. Overwrite, traversal, symlink and backend capability checks still apply. | | Script helpers | echo, printf, true, false, test, [, env, printenv, xargs, expr, date, sleep, timeout. test / [ support special-node and mode predicates and live shell -v, -R, -o, -t queries. Byte-stream descriptors are nonterminal. Ownership queries require the command plugin option predicateIdentity: { effectiveUid, effectiveGid } and backend owner metadata; unavailable capabilities report an error. Remote modes remain advisory. env -v / --debug writes environment changes, the working directory change, and command/argument diagnostics to stderr. | | Changes/review | diff, patch, apply_patch. Diff supports binary comparisons, byte-preserving text changes, symlinks, multi-file comparisons, explicit context widths, and --color[=auto\|never\|always]; diff options and limits describes configuration. |

printf returns status 1 for invalid formats and options, retaining any output written before a format error. Its Bash format dialect accepts zero flags on string conversions and rejects numbered conversions such as %1$s; the grouping flag is accepted without locale-specific separators.

uniq INPUT OUTPUT refuses aliases of the input, including symbolic and hard links, before opening the output. An existing output also requires the filesystem to establish that the files are distinct. cut accepts space/tab separators and padding in range lists, rejects empty comma items, and retains --output-delimiter between adjacent byte or character ranges. chmod -r removes read permissions; --reference=FILE copies a file's mode.

cp -l / --link creates hard links to source files; cp -s / --symbolic-link creates symbolic links using the source operand as the literal target. Relative symbolic targets require destinations in the current directory; use absolute source paths when copying into other directories. Link modes require filesystem link capabilities and support recursive copying, no-clobber and backups. cp -x / --one-file-system creates directories at known filesystem boundaries without copying their contents; unknown filesystem identities remain traversable.

ln -i / --interactive prompts on stderr before replacing an existing destination and reads one answer from stdin. Answers beginning with y or Y confirm replacement; refusal or EOF preserves the entry and returns status 1. The last -i or -f option selects interactive or forced replacement.

ln -L / --logical follows source symlinks for hard links; -P / --physical links the source entry (the default). The last source-mode option wins; both are ignored with -s. Physical symlink hard links depend on backend support and host link semantics on the real filesystem adapter.

Tar creation also accepts --sort=name (bytewise directory-child order; operand order stays unchanged), --sort=none (default), --dereference / -h (archive symbolic-link targets), and --exclude-caches (retain directories with a valid CACHEDIR.TAG and their tag files, omitting other contents). Dereferencing retains backend containment and output-archive checks and rejects directory cycles.

Tar reads newline-delimited exclusion patterns with -X FILE / --exclude-from=FILE. When listing or extracting, --wildcards enables anchored glob member selection; --no-wildcards restores literal selection. --occurrence[=NUM] selects only the requested occurrence of each member operand (default 1), and requires member operands.

Use cat --help, base64 --help, cp --help, sort --help, grep --help, rg --help or tar --help to discover supported options. base64 --version, cp --version and sort --version identify the safe-bash implementation. These informational requests exit before processing input files. cp -u / --update copies missing files and replaces files only when the source has a newer modification time. Recursive copies compare each file separately. File-content copies (cp and cross-device mv) require a retained reader with authoritative file identity. Destinations require streaming writes, exclusive creation for missing or explicitly removed files, or guarded staging for an existing-file move. Exclusive creation buffers the inspected source size within the host input and memory budgets. The reader identity is verified before bytes are read; backends without this guarantee refuse the transfer. Ordinary same-device mv still uses rename. mv -i / --interactive asks on stderr before replacing an existing destination and reads one response from stdin. A response beginning with y or Y allows the move; refusal or EOF keeps both paths and returns status 1. The last of -i, -f, and -n wins. Skipped updates and missing destinations do not prompt, and backups are created only after confirmation. Cross-device mv requires atomic conditional source removal. Backends without this capability refuse the fallback before copying. If a source entry or its parent changes during transfer, cleanup fails and retains the copied data; the command never deletes a replacement source entry. Cross-device mv can replace an existing regular file using guarded staging with atomic destination and ancestry checks and retained staging cleanup. Memory mounts support this route. Source bytes are buffered under the input and collection limits; supported modes and timestamps are prepared before publication. Backends without these guarantees refuse the overwrite. Hardlinked destinations are replaced without changing the contents of their sibling links. Opaque destination identities remain unsupported. Backends with bound path resolution support symlinked destination parents and reject path changes before publication. Same-device replacement and cross-device moves to missing destinations remain supported. Directory moves with multiple source links to one inode require atomic unlink receipts to remove those links without accepting unrelated source changes. cp -i / --interactive prompts on stderr before overwriting each existing file and reads one response from stdin. Responses beginning with y or Y allow replacement; refusal or EOF preserves the destination and returns status 1. The last -i or -n wins; -f does not disable confirmation. stat -t FILE / --terse prints file metadata in GNU field order, using ? for fields the backend does not expose. -c / --format and --printf override terse output. Filesystem terse output (-ft) remains unsupported. factor --exponents 72 prints 72: 2^3 3^2; without operands, it reads numbers from stdin. Integers are exact beyond JavaScript's safe-integer range. factor.limits.maxValue accepts a positive safe integer, a positive bigint, or Infinity (the default). zstd, unzstd, and zstdcat accept -q / --quiet, including combined short options such as -qc. Repeating quiet suppresses processing errors on stderr while preserving failure exit codes and validation. Use --[no-]check to control frame checksums, --stream-size=BYTES to declare and enforce input size, and --[no-]pass-through to copy unrecognized input during decompression. --exclude-compressed skips compressed file suffixes. Single-thread execution, I/O read-ahead, literal compression, row matching, size hints and bounded long-distance matching are configurable; see the compression options. Named compression removes its source only through an atomic identity- and ancestry-bound conditional delete. Backends without that guarantee refuse before publishing output; use --keep or --stdout to retain the source. Zstandard presets are parsed as whole numbers; the bounded codec supports levels 1–9. Higher presets and --fast[=NUM] fail explicitly instead of selecting a different level. Native codecs enforce a 64 MiB allocation ceiling for encoding and decoding, including ZIP LZMA; Zstandard windows are limited to 32 MiB. XZ decompression flags can lower the ceiling. Output-byte limits remain separate. Use grep -A NUM, -B NUM or -C NUM to include lines after, before or around each match; separated groups use --, and -n marks context lines with -. grep -L / --files-without-match prints filenames without selected lines. Its exit status follows GNU grep: 0 if any input line is selected, 1 otherwise, even when it prints a filename. -v inverts line selection; -q suppresses output. This also applies to egrep and fgrep. Use grep -r to search directories, with --include, --exclude, --exclude-from and --exclude-dir to filter basenames. -R follows nested symlinks; recursion is bounded to 128 levels and detects ancestor loops. -b prints byte offsets, -Z uses NUL after filenames, and --no-group-separator hides context separators. These options also work with egrep and fgrep. --group-separator=SEP customizes context separators, --label=LABEL names stdin, and --initial-tab inserts a tab after prefixes. --binary-files=text searches raw bytes as text; other binary-detection modes are explicitly unsupported. --binary preserves bytes without Windows text-mode translation. --color=never and --colour=never disable colour; auto also emits plain text because output is captured rather than a terminal. Forced colour is unsupported. The default bounded grep matcher rejects BRE groups, intervals, backreferences and escape extensions such as \|. Use grep -E 'Remove upvote|Upvoted' for alternation or grep -F -e 'Remove upvote' -e 'Upvoted' for literal alternatives. Exit 1 means no match; exit 2 means filtering failed. If a preceding action succeeded, inspect its resulting state and retry only the read-only verification before repeating the action. A configured regex executor may support more syntax. tee accepts -i / --ignore-interrupts, -p, and --output-error[=warn|warn-nopipe|exit|exit-nopipe] alongside -a / --append. File write failures continue with the remaining destinations in warn modes and stop in exit modes; the last error option wins. Bare --output-error and -p select warn-nopipe. Virtual commands have no native signal handlers or OS pipes: host cancellation and shell downstream-failure handling remain in effect. iconv -f UTF-8 -t UTF-16BE -o result source writes converted bytes to a virtual file, replacing its existing contents. --output=result is equivalent; omit input files to convert stdin, and use --output=- to write to stdout. Conversion output limits also apply to file output.

iconv -s / --silent accepts the native silent control while retaining fatal error diagnostics. --verbose prints each named input as filename: on stderr before conversion; stdin has no progress label.

Use grep -w or --word-regexp to match whole words with C-locale byte matching (word characters are ASCII letters, digits and underscore), including fixed strings and -o output.

cut -c preserves and selects individual bytes in C/POSIX locales, using the first nonempty value of LC_ALL, LC_CTYPE, and LANG. Other locales and an unset locale retain UTF-8 character selection.

Use strings -s ':' or --output-separator=: to separate extracted strings with custom text, including after the final string. An empty separator joins the strings. Default rg accepts UTF-8 literals and bounded ASCII regex operators (., anchors, classes, groups, alternation and greedy repetition), including -o and match counts. It preserves original UTF-8 byte offsets and supports case/word selection on ASCII subjects, plus bounded ASCII globs for path and ignore filtering. Unicode case/word selection, Unicode regex syntax, escape extensions, lazy repetition and invalid UTF-8/NUL subjects require a configured regex executor; unsupported profiles fail explicitly. Literal replacement, trimming, file-size limits, depth aliases and explicit virtual ignore files are supported. --threads accepts a count while execution stays serial; --multiline admits line-compatible searches, with cross-line patterns still rejected by the bounded matcher.

Default grep, egrep and fgrep preserve arbitrary subject bytes for fixed matching and ASCII patterns without regex syntax, including NUL and invalid UTF-8 records with -a. Patterns retain UTF-8 validation; regex operators retain the bounded engine's UTF-8 subject restrictions.

/commands/fmt exports parseFmtArguments, the pure byte coroutine createFmtEngine, and equivalent fmtCommand({ limits?, profile? }) / fmt(context, { width?, goal?, crown?, tagged?, split?, uniform?, prefix?, files?, limits?, profile? }) execution APIs, with literal byte arguments available as an alternative to typed formatting options. fmtCommands({ limits?, profile?, replace? }) registers an explicit plugin. Formatting defaults to GNU coreutils 9.10 byte lengths and bounded paragraph optimization; an explicit historical 8.30 profile retains its older width boundary. Supports -w, -g, -c, -t, -s, -u and -p; for example, printf 'aa bb cc dd ee' | fmt -w8 produces aa bb cc\ndd ee\n. Private implementation and declarations ship inside safe-bash. See the fmt contract for spacing, prefix, cancellation and resource limits.

Opt-in commands and storage

These plugins are separate from agentCommands(); pass them to shell.use(...).

mikeYqCommands() from /commands/yq enables bounded Mike-style format conversion: yq -p csv -o json . records.csv reads a table, and yq -o csv . records.yaml writes one. Input and output support CSV, TSV, properties, XML, INI, TOML, base64 and URI; shell and Lua are output only. See the format profile for supported shapes and limits.

Import csvcutCommands from @poe-platform/safe-bash/commands/csvcut and pass it to shell.use(csvcutCommands()) to enable CSV projection. For example, printf 'a,b\nx,y\n' | csvcut -c2,1,2 emits b,a,b\ny,x,y\n. The same subpath exports the typed SDK and bounded record/selector APIs; see the flags, limits and profiles. Packed Node ESM consumers are verified; actual browser/workerd execution remains unqualified.

| Command | Plugin and configuration | | --- | --- | | dos2unix, unix2dos | lineEndingCommands({ limits?, replace? }) from /commands/line-endings: byte and UTF-16 line-ending conversion. Use -e / --add-eol to terminate the final line, -O / --to-stdout to convert without rewriting inputs, and -i[FLAGS] / --info[=FLAGS] to inspect line endings, BOM and text/binary status. -v prints conversion details. In-place conversion requires safe publication capabilities and preserves reported ownership; --allow-chown permits publication when ownership cannot be preserved, and --no-allow-chown restores the default. | | curl, wget | networkCommands({ authorize, transport?, limits?, replace? }): required authorization on every request, redirect, and retry. Curl's -r/--range requests byte ranges. Wget supports --header 'NAME: VALUE', --user-agent, --referer, --post-data/--post-file and --method with --body-data/--body-file; body files come from the VFS and retain their bytes. Repeated wget headers replace earlier values by name; --header='' clears custom headers, and NAME: sends an empty value. Transport-controlled headers are rejected, and custom headers are dropped after crossing origins. Node uses the native HTTP transport and supports curl's --connect-timeout for DNS/TCP/TLS setup. Workers can inject createFetchTransport(), which cannot enforce a separate connection timeout. createOriginAuthorizer([...]) provides exact origin/hostname policy; its omitted allowlist is deliberately * (allow all). Options and limits. | | node | nodeCommands({ runtime, limits?, replace? }): runs JavaScript with an injected SafeJS runtime, virtual files, and shell streams. Usage and supported subset. | | python, python3 | pythonCommands({ createExecutor }) from @poe-platform/safe-bash/commands/python: supply an explicit executor for invocation-local Python, filesystem I/O and shell streams. Open-file, directory, worker-capacity, input-fragment, package-download and cache quotas are unlimited by default; configure individual budgets when needed. Executor and ownership contract. | | llm | llmCommands({ providers, defaultModel?, replace? }): opt-in model routing, sandbox attachments and streamed text/binary output. Includes injected-transport OpenAI and ElevenLabs reference providers. Configuration and provider contract. | | pdfinfo | /commands/pdfinfo: opt-in pdfinfoCommands({ replace? }) registering pdfinfo, pdftoppm, pdfimages, pdfunite, and pdfseparate powered by @poe-code/pdf-ast. Supports Poppler metadata, page geometry, -box, -isodates/-rawdates, -custom, -meta, -url, -js, -struct/-struct-text, -dests, page rasterization (pdftoppm), image listing/extraction (pdfimages), PDF merging (pdfunite), and page splitting (pdfseparate). | | pdftoppm | /commands/pdftoppm: opt-in pdftoppmPlugin({ replace? }) and createPdftoppmCommand powered by @poe-code/pdf-ast. Renders PDF pages to .png, .ppm, .pgm, .pbm, or .svg with configurable -r/-rx/-ry DPI, -f/-l page ranges, -singlefile, -sep, and -x/-y/-W/-H/-cropbox sub-region cropping. | | pdfimages | /commands/pdfimages: opt-in pdfimagesPlugin({ replace? }) and createPdfimagesCommand powered by @poe-code/pdf-ast. Lists (-list) and extracts embedded XObject, nested Form XObject, and inline images to .png, .jpg (-j/-all), .ppm, or .pbm with CTM PPI metadata and -p page numbering. | | pdftotext | /commands/pdftotext: opt-in pdftotextCommands({ replace? }) registering pdftotext and pdftohtml powered by @poe-code/pdf-ast. Supports logical reading order with dehyphenation, -layout, -raw, -bbox/-bbox-layout XHTML, Poppler -tsv, -htmlmeta, and pdftohtml (-xml, -stdout, -s, -c). | | pdftk | /commands/pdftk: opt-in pdftkPlugin({ replace? }) and createPdftkCommand powered by @poe-code/pdf-ast. Supports cat/shuffle multi-handle page assembly and rotations, dump_data_fields_utf8, fill_form (FDF/XFDF/stanzas) with flatten, burst, rotate, background, and stamp. | | qpdf | /commands/qpdf: opt-in qpdfCommands({ replace? }) and runQpdfCli powered by @poe-code/pdf-ast. Supports --check, --show-npages, --show-xref, --show-object, --json, --is-encrypted, --requires-password, --show-encryption, --encrypt/--decrypt, --empty --pages ... --, --split-pages, --rotate, --qdf, and --replace-input. | | magick, convert, mogrify, composite, montage, compare | /commands/imagemagick: opt-in imagemagickPlugin({ replace? }) (imagemagickCommands) registering ImageMagick magick, convert, mogrify, composite, montage, compare, and identify powered by @poe-code/image-ast. Supports xc:/canvas:/gradient:/radial-gradient:/pattern:checkerboard/label: generators, geometry modifiers (!, >, <, ^, %, @), left-to-right image stack operators (-resize, -crop, -extent, -border, -shave, -splice, -chop, -roll, -trim, -rotate, -flip, -flop, -shear, -distort (SRT, Perspective, Affine, Barrel), -swirl, -implode, -wave, -shadow, -vignette, -negate, -colorspace, -sepia-tone, -solarize, -posterize, -colors, -modulate, -gamma, -level, -threshold, -opaque/-transparent, -evaluate, -function, -clut, -fx, -blur, -sharpen, -edge, -emboss, -charcoal, -draw (rectangle, circle, polygon, polyline, bezier, path, text), -annotate, ( ... ), +clone, -morph, +append, -append, -composite, -flatten), compare -metric AE\|MAE\|MSE\|RMSE\|PSNR\|SSIM, in-place mogrify, montage grids, out-%d.png scene output, and stdin/stdout byte streams (-, png:-). | | sips | /commands/sips: opt-in sipsPlugin({ replace? }) (sipsCommands) registering macOS sips and ImageMagick identify powered by @poe-code/image-ast (sharp). Supports -g, -1, -Z, -z, --resampleWidth, --resampleHeight, -c, --cropOffset, -p, --padColor, -r, -f, -s format, -s formatOptions, -o/--out, and identify (-ping, -format, -verbose). | | soffice | /commands/soffice: opt-in sofficeCommands({ replace? }) registering soffice and libreoffice powered by @poe-code/pdf-ast. Supports headless --convert-to pdf[:writer_pdf_Export\|calc_pdf_Export\|impress_pdf_Export] (including JSON FilterData), StarCalc --convert-to csv:..., --cat, and conversion across .docx, .odt, .rtf, .xlsx, .pptx, .csv, .html, .png, .txt, and .pdf. | | wkhtmltopdf | /commands/wkhtmltopdf: opt-in CLI/SDK adapter with VFS byte I/O and explicit limits. Requires a supplied first-party static renderer; none is included. wkhtmltopdfCommands({ limits, renderer? }) and runWkhtmltopdf(context, options) share behavior. Exported switches lists all 122 flags and rejections; wkhtmltopdfLimits defaults to 16 MiB PDF output, 64 objects and 128 batch jobs. --help, --extended-help and --version work without a renderer; TOC and dynamic execution are unavailable. | | unrtf | /commands/unrtf: opt-in unrtfCommands({ limits?, replace? }), equivalent unrtf(context, { format?, file?, limits? }) SDK, and bounded tokenizeRtf, extractRtf, renderRtf streams. Strict UTF-8 text/HTML supports scoped font/color/emphasis and flat tables; objects, pictures and field instructions stay inert. Use unrtf --text /document.rtf or unrtf --html /document.rtf (HTML default); output is UTF-8 with no invented text separator/final LF. Accepted flags are --text, --html, --quiet, --nopict, -n, --; unsupported profiles fail with status 1. Opt into scoped GNU 0.21.10 text/HTML/LaTeX wrappers and character aliases with --profile=gnu-0.21.10 or SDK profile; this admits --latex, --noremap (SDK noremap) and banner suppression via --quiet (SDK quiet). Strict extraction remains in effect; full native parser/rendering parity and picture exports remain unimplemented. Private implementation/types ship inside safe-bash; see the flags, limits and runtime profile. | | exiftool | /commands/exiftool: opt-in exiftoolCommands({ limits? }) for uncompressed PNG text and tIME inspection/selected writes. Use exiftool -j -Title /image.png, -csv for union headers, or -Title=Example to edit with an _original backup. Typed SDK argv uses createExiftoolArguments. Defaults: 64 files, 16 MiB cumulative input/output, 8 MiB decoded and 32 MiB retained bytes. Private implementation/types ship inside safe-bash. PDF, Office, EXIF/XMP, broader timestamps, import and execute protocols remain unsupported; see the supported profile. | | csvcut | /commands/csvcut: opt-in csvcutCommands({ limits?, replace? }) and equivalent csvcut(context, { include?, exclude?, zero?, names?, headerless?, deleteEmptyRows?, lineNumbers?, addBom?, dialect?, filePath?, encoding?, help?, version? }, { limits? }). Bounded UTF-8-sig byte input, comma/LF output and literal VFS paths; csvkit 2.2.0 selector candidate, permissive-v1 reader, quoting 0/3 only, no Sniffer or full Python compatibility. Defaults: 16 MiB input, 32 MiB output, 64 MiB retained, 1 MiB fields and 100,000 cells. Private implementation/types ship inside safe-bash; see the flags, limits and runtime profile. | | csvgrep | /commands/csvgrep: opt-in csvgrepCommands({ limits?, replace? }) and equivalent csvgrep(context, { columns, match?, regex?, file?, any?, invert?, dialect?, filePath? }, { limits? }). Preserves CSV rows/headers with UTF-8-sig input, comma/LF output and the bounded bounded-sequence-v1 Python regex subset; full csvkit compatibility is unqualified. Private implementation/types ship inside safe-bash; see the supported profile. | | fold | /commands/fold: opt-in foldCommands({ locale?, limits?, replace? }) and fold(context, { width?, mode?, spaces?, files?, locale?, limits? }) for byte streams and literal VFS paths. Supports column, character and byte counting with a pinned Unicode 17 profile or explicit C decoding. Use replace: true with agentCommands() to replace its existing fold implementation; defaults are unchanged. Private implementation/types ship inside safe-bash; see the supported profile. | | playwright-cli | Standard browser commands, storage state, native snapshots, recordings, and traces through a host-owned adapter. Completed actions retain their live session when a failed checkpoint confirms safe cleanup. Sessions and host capabilities. Optional Cloudflare adapter and portable profiles. installPlaywrightNetworkPolicy from /playwright supports browser-native redirects with per-hop bounded host HTTP fetch and independent direct HTTP/WebSocket denial. bindPlaywrightRoutePolicy(context, { ownsRequest, admit, fetch }, limits) lets standard route mocks and header rewrites use that host policy, with admission before matching and bounded response leases. Cloudflare guardrails do not establish WebRTC/UDP denial or all-protocol accounting; hosts requiring those guarantees must refuse this integration. Network policy and lifecycle. |

/commands/diff3 provides opt-in diff3Commands({ limits?, replace? }), the equivalent diff3(context, { files, merge?, selector?, labels?, ... }) SDK, and pure byte report/merge/ed and analysis APIs. Its GNU 3.12 qualified profile uses VFS files and bounded stdin spooling; private implementation and declarations ship inside safe-bash. diff3 -m /ours /base /theirs emits merge bytes (flagged conflicts return 1); the default report returns 0 for differences. -e emits an ed script without executing it. GNU 3.12's -X is unflagged; one stdin operand is supported in any position, and external --diff-program selection is refused. The command is qualified for Node ESM; actual browser/workerd engines remain unverified. See the flags, limits and supported profile.

/commands/htmlq exports opt-in htmlqCommands({ limits?, replace? }), equivalent htmlq(context, { selector: "p", text: true }) SDK execution (or literal argv) and the inert HTML byte-stream engine. Use htmlq 'div > p' -t -f /input.html; attribute, text and HTML projections preserve pinned separators and lazy first-match removals. Explicit engine limits and cancellation are required. Private implementation and types ship inside safe-bash. Full HTML5 recovery, selector grammar and Rust URL parity remain unqualified; modern :is/:where/:has/:lang are explicitly rejected. Use --attributes (plural) for attribute output; no-match succeeds with empty output. Scripts/styles remain inert and preserved; interior BOMs are retained regardless of input chunking, correcting the pinned upstream defect. Node.js 22+ is qualified; browser/workerd conditional graphs are checked in Node, while actual engines remain unverified. See the supported flags and limits.

For example, load the optional PDF adapter to inspect its supported options:

import { Shell, createMemoryFileSystem } from "@poe-platform/safe-bash";
import { wkhtmltopdfCommands } from "@poe-platform/safe-bash/commands/wkhtmltopdf";

const shell = new Shell({ fs: createMemoryFileSystem() }).use(wkhtmltopdfCommands());
try {
  console.log((await shell.exec("wkhtmltopdf --help")).stdout);
} finally {
  await shell.dispose();
}

For a custom same-isolate Python JSPI host, use createPythonJspiExecutor from the same Python entry with an explicit loader, precompiled Wasm modules and pinned, authenticated runtime assets; follow the static host recipe. Native I/O uses the caller's asynchronous filesystem without workspace copying, Node worker threads or a SAB request/reply bridge. This path is qualified with installed public-package artifacts in local workerd, not a verified Cloudflare deployment or a managed Python native-filesystem integration. JSPI cancellation is cooperative; it neither preempts CPU-only loops nor establishes confinement.

Storage can be in memory, a rooted host directory, S3-compatible storage, or WebDAV, with read-only wrappers, mounts, and overlays. Choose and configure it explicitly; see the filesystem guide.

Run JavaScript with SafeJS

Plug SafeJS into node; nothing starts a native Node.js subprocess or loads a runtime automatically. SafeJS is the execution engine, not a separate shell command.

import { Shell, agentCommands, createMemoryFileSystem, nodeCommands } from "@poe-platform/safe-bash";
import { Budget, run, makeFsModule, declareHostOperation, parseSourceModule } from "@poe-platform/safe-js";

const fs = createMemoryFileSystem();
await fs.writeFile("/transform.js", new TextEncoder().encode(`
  import { writeFile } from "fs";
  const text = await process.stdin.readText();
  await writeFile("/result.txt", text.toUpperCase());
  console.log(process.argv[2]);
`));

const shell = new Shell({ fs }).use(agentCommands()).use(nodeCommands({
  runtime: {
    run, makeFsModule, declareHostOperation, parseSourceModule,
    createBudget: options => new Budget(options),
  },
}));

try {
  const result = await shell.exec("printf 'hello\\n' | node /transform.js done; cat /result.txt; node -p '1 + 2'");
  console.log(result.stdout);
} finally {
  await shell.dispose();
}

Output: done\nHELLO\n3\n.

node -e SOURCE evaluates a program; node -p EXPRESSION prints an expression. node FILE, node -, and bare node accept virtual-file or stdin source. --env-file loads virtual dotenv files; --version uses supplied runtime identity and --completion-bash lists supported options. Runtime flags require explicit adapter capabilities; see the Node configuration. Programs get console, virtual process.argv, process.env, process.cwd(), process.exitCode, guest-owned Buffer bytes with string encodings and shared views, and shell streams. Await process.stdout.write(text) and process.stderr.write(text); read input with process.stdin.readText() or readBytes(size?). These are bounded async helpers, not native Node streams. setTimeout(callback, delay?, ...args) and clearTimeout(id) support cancellable guest timers, including during top-level await. Pending callbacks finish before the command exits and share its deadline and interpreter budgets. require("fs").readFileSync(path, "utf8") reads text from the VFS before the next guest statement; node:fs and named/default/namespace imports work too. An encoding is required. Async helpers remain available through fs.promises and fs/promises.

Import async filesystem functions from "fs" or "node:fs/promises", or use const fs = require("node:fs/promises"). require("./data.json") loads virtual JSON relative to the entry file's directory, or virtual cwd for inline and stdin source. node --require ./setup.cjs / node -r ./setup.cjs preloads virtual CommonJS modules before the program; repeated flags run in order from virtual cwd. Explicit .cjs, .js, and .json module paths share an invocation-local cache, including nested relative dependencies, and retain source limits, interpreter budgets, and cancellation. Other synchronous fs operations, package search, ESM loading, process.exit(), and native module fallback are unavailable. Pass limits for source/input/output bytes, timeout, and interpreter budgets; see defaults and configuration.

Wire an individual MCP tool

There is no generic safe-bash MCP server. Define each server and tool around the specific operation it grants, then call Shell.exec() with fixed shell source. For example, this server exposes name normalization without accepting arbitrary Bash from the MCP client:

import { Shell, agentCommands, createMemoryFileSystem } from "poe-code/safe-bash";
import { createServer, defineSchema } from "tiny-stdio-mcp-server";

const shell = new Shell({ fs: createMemoryFileSystem() }).use(agentCommands());
const input = defineSchema({ names: { type: "string" } });

const server = createServer({ name: "contacts-tools", version: "1.0.0" })
  .tool("normalize_names", "Sort and deduplicate newline-separated names", input,
    async ({ names }) => {
      const result = await shell.exec("sort | uniq", { stdin: names });
      if (result.exitCode !== 0) throw new Error(result.stderr);
      return result.stdout;
    });

try {
  await server.listen();
} finally {
  await shell.dispose();
}

Install poe-code and the MCP transport package in that server's own project. Choose its filesystem, command bundle, limits, and opt-in capabilities there; do not expose caller-supplied shell source unless arbitrary shell execution is the deliberate API.

Add a command

A CommandDefinition has a name and an execute(context) handler. This example adds file-bytes, which reports a virtual file's size without reading its contents:

import {
  Shell, agentCommands, createMemoryFileSystem, resolvePath, writeText,
  type CommandDefinition,
} from "@poe-platform/safe-bash";

const fileBytes: CommandDefinition = {
  name: "file-bytes",
  async execute({ args, cwd, fs, stdout, stderr, signal }) {
    if (args.length !== 1) {
      await writeText(stderr, "Usage: file-bytes FILE\n");
      return { exitCode: 2 };
    }
    const stat = await fs.stat(resolvePath(cwd, args[0]!), { signal });
    await writeText(stdout, `${stat.size}\n`);
    return { exitCode: 0 };
  },
};

const shell = new Shell({ fs: createMemoryFileSystem() }).use(agentCommands());
shell.use({
  name: "file-tools",
  setup(host) { host.commands.register(fileBytes); },
});
try {
  const result = await shell.exec("printf 'hello\\n' > message.txt; file-bytes message.txt | cat");
  if (result.exitCode !== 0) throw new Error(result.stderr);
  process.stdout.write(result.stdout);
} finally {
  await shell.dispose();
}

Output: 6\n. For a single command, use shell.register(fileBytes) instead of a plugin. Duplicate names fail unless registration explicitly sets { replace: true }. Handlers receive args, stdin, stdout, stderr, cwd, env, fs, and signal; return { exitCode } with an integer from 0–255, await writes, and pass the signal to I/O. Use context.invoke to call another command with literal arguments rather than interpolating shell source. Command contract.

shell.use(middleware) wraps command dispatch for logging or policy checks; middleware must await or return next(). Plugins can also register filesystem factories and provide a dispose() hook. Plugin contract. For SafeJS host integration, makeSafeJsShellModule exposes shell execution and makeSafeJsFsModule adapts the filesystem through injected runtime hooks. Integration contracts.

Options

Shell and execution

| new Shell(...) option | Behavior | | --- | --- | | fs | Required filesystem; no implicit host access. | | deviceView | "default" (the default) adds synthetic /dev/null and shadows ordinary backing files there. "provided" uses the supplied filesystem's paths and capabilities, including an exec filesystem override, without adding devices. Nested commands retain the selected view. | | cwd | Initial virtual directory; defaults to /. | | env | Initial exported variables; defaults to an empty map, with PWD set from cwd. No host environment inheritance. Never pass host process.env or any secret-bearing object: everything in env is readable by executed scripts (env, printenv, $VAR), and on Cloudflare Workers with nodejs_compat process.env contains the Worker's secret bindings. The shell warns only for the identical host process.env object, not copies, and does not filter values. | | commands | Existing CommandRegistry; defaults to an empty registry. | | limits | Resource limits listed below. |

exec(source, options) can override fs, cwd, and limits, and merge env for one execution. stdin accepts a string, Uint8Array, or async byte source; stdout/stderr accept byte sinks. Results still buffer output when sinks are provided. Pass an AbortSignal as signal to cancel. Option types.

Execution quotas are unlimited by default. Set individual limits to opt in; supplying one does not enable other quotas. Available quotas are maxParseUnits, maxInputBytes, maxOutputBytes, maxCommands, maxFileSystemOperations, maxPathComponents, maxPathnameComponents, maxRedirects, maxPipelineStages, maxLoopIterations, maxSubstitutionDepth, maxSourceBytes, maxExpansionFields, maxExpansionBytes, maxWallClockMs, and maxCpuMs. The CPU deadline measures elapsed time including waits at cooperative checkpoints. pipeHighWaterMark defaults to 64 KiB for streaming backpressure and does not cap total work. cloudflareWorkerLimits is an explicit restrictive preset. Background job quotas (maxJobs, maxWaiters, maxCleanupsPerJob) are also unlimited unless supplied.

Always call dispose() when finished. Shell failures normally produce an exit code and stderr; limit violations, cancellation, and host failures can reject exec(). The command budget counts compound commands and loop conditions as well as body commands. With both work budgets set to 10,000, while true; do :; done reaches maxCommands first. Work budgets bound execution counts, not elapsed latency. maxPathComponents counts PATH search directories consulted per lookup; maxPathnameComponents separately bounds components within each filesystem path. Await execution settlement and shell disposal before closing backing storage, including after a caller timeout; cancellation is cooperative.

Portable browser profiles restore blank tabs by default, preserving storage, settings, tab count, and selection. Only pass tabRestoration: 'navigate' to restoreBrowserProfile when replaying saved URLs is authorized; action URLs can repeat effects. Use recovery: true to also suppress configuration and provider scripts. Owner-bound hosts can enable namedSessionAttachment: true on createPlaywrightCli({ adapter, persistence, ... }) to support attach NAME. It selects an existing live session or restores its committed resumable profile for subsequent invocations with the same PLAYWRIGHT_CLI_SESSION default, including an authenticated agent ID different from the target name. Explicit -s=NAME overrides selection; detach retains the browser, while close retires it. See the host capability contract. Hosts accepting direct browser activity can call renewSession({ name, context }) to renew that exact retained session's idle deadline without browser commands. Browser snapshots have no byte limit, including automatic snapshots after navigation. Legacy maxSnapshotBytes settings are ignored. playwright-cli --help and playwright-cli show --help report dashboard availability. Without a host dashboard ability, interactive local browser login is unavailable; use the authentication flow supplied by the host application.

For Cloudflare Workers, the exported cloudflareWorkerLimits profile also sets commandLimits.archive: 4 MiB archive inputs, an 8 MiB ZIP input collection peak, 4 MiB entries, 8 MiB total payload and 1,000 members. These ceilings apply to tar, zip and unzip, including nested calls and agentCommands(); tighter registration limits still win. Oversized ZIP file metadata is rejected before reading, and growing streams remain byte-bounded. ZIP decoding retains a bounded archive in memory; codec workspace, parsed entries, filesystem storage and other requests need additional headroom. Configure other command-family buffers at no more than 8 MiB and bound concurrent requests at the host. Create a separate Shell, environment object, and quota-wrapped filesystem view for each tenant or request. Never reuse tenant state across requests; import withFileSystemQuota from poe-code/safe-fs to bound cumulative writes, including command-initiated copies and streaming output. Admission control and rate limiting remain host responsibilities.

Command configuration

agentCommands() accepts replace (default false), an execute fallback for nested command dispatch, and regex worker limits. Per-family options are text, structured, search, diffPatch, metadata, archive, tableText, streamInspection, streamFormat, split, timeEnv, tree, file, column, htmlToMarkdown, du, expr, which, timeout, and applyPatch. Use the typed options and linked family interfaces for their individual limits and hooks, including clocks and schedulers. Family budgets are separate from shell counters; limits.commandLimits.archive supplies invocation ceilings. Per-execution family overrides merge with the shell's profile, and non-Worker hosts can configure larger limits. replace applies across the entire bundle. Text, search, structured queries, directory inspection, table/stream tools, time/environment commands, metadata, diff/patch, LLM providers, yes, less, sponge, bc, htmlq, which, getopt, factor, and tsort have unlimited resource budgets by default. Set individual family limits to opt in; setting one limit leaves the others unlimited. Explicit Infinity also disables a limit. xargs -s Infinity and --max-chars=Infinity explicitly remove the command-size quota. Stream chunk sizes and polling intervals control execution independently of these quotas. diff -u /dev/null FILE and its reverse produce creation/deletion patches; top-level readable character and FIFO inputs are read to EOF. Regular files retain identity-checked reads.

The package root exports createBoundedRegexProvider, BoundedRegexProvider, and BoundedRegexProviderOptions. agentCommands() uses this provider by default; pass regexExecutor to configure its resource limits explicitly:

import { agentCommands, createBoundedRegexProvider } from "@poe-platform/safe-bash";

shell.use(
  agentCommands({
    regexExecutor: createBoundedRegexProvider({ maxWorkers: 1, maxInputBytes: 65_536 }),
    regex: { maxWorkers: 1 }
  })
);

Provider limits bound pattern/input/result bytes, matches, work, allocations, states, and active workers. regex configures executor queue and timeout limits. The default provider runs cooperatively; it does not provide native-worker or process-memory isolation. No internal-module import is needed.

Environment variables

There are no package-specific runtime environment switches. Supply these through env or set/export them inside a script; they refer to the virtual environment:

| Variables | Effect | | --- | --- | | HOME, CDPATH, PWD, OLDPWD | Home expansion, directory search, current and previous directory. The shell maintains PWD/OLDPWD on directory changes. | | PATH | Virtual script lookup and which; never a host executable search. | | IFS | Field splitting and read; defaults to space, tab, and newline. | | LC_ALL, LC_CTYPE, LC_COLLATE, LANG | Character and collation behavior where supported; locale support varies by command. | | TMPDIR | mktemp directory; defaults to /tmp, which must exist in the VFS. | | TZ | date and touch timezone. date otherwise uses timeEnv.defaultTimeZone; touch defaults to UTC. | | QUOTING_STYLE | stat filename quoting: literal, shell-always, or shell-escape-always. |

getopts starts with OPTIND=1 and OPTERR=1, updates OPTIND/OPTARG, and honors changes made in the script. PIPESTATUS exposes pipeline stage statuses. curl does not read proxy variables, host credentials, .curlrc, or .netrc. curl --cacert VFSFILE reads a bounded PEM CA bundle from the virtual filesystem and scopes its trust to that transfer, retaining certificate and hostname verification. The Node transport supports it; Fetch rejects custom CA trust. Explicit -K/--config files come from the VFS. Curl also supports --url-query, negated boolean flags and --variable/--expand-* value options. HTTP version selection and ignored Content-Length require explicit transport capabilities; Node supports --http1.1. See the network options. Following curl 8.5/8.10, --data-urlencode name@file sends no field when the file is empty; use --data-urlencode name= to send an explicit empty value. Repeated curl data options use curl 8.10.1 joining semantics: & is added only when earlier fragments have produced bytes, so empty leading fragments add no separator. curl --json retains its Content-Type: application/json and Accept: application/json headers when -G or a 301/302/303 redirect removes the body. Explicit -H overrides and suppression still apply within the original origin; cross-origin custom-header and credential protections remain in effect.

Limitations

  • This is a Bash-like interpreter, not full Bash or POSIX certification. No background jobs/job control, exec, process substitution, associative arrays, or C-style for ((…)) loops. shopt supports dotglob, globstar, nullglob, nocaseglob, and nocasematch, plus -o for supported set options. extglob can be queried, printed, or unset; enabling it is unsupported.
  • Utilities implement subsets of their native counterparts' flags and behavior. There is no git, npm, npx, or fallback to installed host programs. The opt-in node command is not a general Node.js runtime.
  • Plugins, filesystem adapters, and runtime providers are trusted host JavaScript, not sandboxed code. Real storage and network plugins grant real access; URL allowlisting alone does not pin DNS or prevent access to private addresses.
  • Cancellation is cooperative; it cannot undo completed effects or stop opaque host work. Node Shell runs active timeout -k / --kill-after children in a terminable worker and returns 137 after hard escalation. Custom commands, middleware and extensions require explicit workerModules factories. Portable hosts require their own escalation policy. Nested worker escalation and finite shared interpreter quotas are refused; see the timeout profile. Limits do not bound total process memory. timeout --preserve-status retains the child's status; --signal (-s) accepts Linux signal names and numbers and sets the cancellation exit status. trap supports cleanup and inherited trap inspection by default. Host signal delivery requires the optional trap extension signal host. Signals from timeout use cooperative cancellation unless a kill-after worker policy is active; signal 0 lets the child finish, and KILL reports status 137 on expiry.