jsh - JavaScript shell scripts (.jsh files)
OVERVIEW
.jsh files are JavaScript shell scripts that are auto-discovered as commands anywhere on the VFS. Place a file named greet.jsh on the filesystem and it becomes the shell command greet. The command name is the filename without the .jsh extension.
Discovered scripts appear under "User scripts (.jsh)" in the commands output. Discovery scans /workspace/skills/ first (giving skill-shipped scripts priority), then the rest of the VFS. The first .jsh file found for a given basename wins.
GLOBALS
Scripts run in an async wrapper with these globals available:
-
processprocess.argv— argument array (["node", scriptPath, ...userArgs]).process.env— environment variables (snapshot).process.exit(code)— exit the script with a status code.process.stdout.write(str)— write to stdout.process.stderr.write(str)— write to stderr.process.cwd()— current working directory. -
consoleconsole.log(),console.info()write to stdout.console.error(),console.warn()write to stderr. -
fsVFS bridge object. All methods are async — always
awaitthem.await fs.readFile(path)— read file as stringawait fs.readFileBinary(path)— read file as Uint8Arrayawait fs.writeFile(path, content)— write string to fileawait fs.writeFileBinary(path, bytes)— write Uint8Array to fileawait fs.readDir(path)— list directory entriesawait fs.exists(path)— check if path existsawait fs.stat(path)— returns{ isDirectory, isFile, size }await fs.mkdir(path)— create directory (recursive)await fs.rm(path)— remove file or directory (recursive)await fs.fetchToFile(url, path)— download URL to VFS, returns byte count
Relative paths are resolved against the current working directory.
-
exec(command)Run a shell command via the WASM bash interpreter. Returns
{ stdout, stderr, exitCode }. Alwaysawaitit.
TOP-LEVEL AWAIT
Script bodies are wrapped in an AsyncFunction, so top-level await works directly. Use it for all async operations.
NEVER use .then() — the function body exits before promise chains resolve, causing callbacks to silently produce no output. Always use await instead.
// WRONG — silent failure, no output
fs.readFile('/workspace/data.txt').then(content => {
console.log(content);
});
// CORRECT — always await
const content = await fs.readFile('/workspace/data.txt');
console.log(content);
EXAMPLE
A script at /workspace/skills/my-skill/wordcount.jsh becomes the command wordcount:
// wordcount.jsh — count words in a file
const filePath = process.argv[2];
if (!filePath) {
console.error('usage: wordcount <file>');
process.exit(1);
}
const text = await fs.readFile(filePath);
const words = text.trim().split(/\s+/).length;
const lines = text.split('\n').length;
console.log(`${words} words, ${lines} lines`);
SKILL INTEGRATION
Skills can ship .jsh scripts alongside their SKILL.md. These scripts are discovered automatically and become available as shell commands when the skill is installed. This is the primary way skills expose new command-line tools to the agent.
DUAL-MODE EXECUTION
.jsh scripts work in both CLI/standalone mode and Chrome extension mode. In CLI mode, scripts execute via AsyncFunction constructor. In extension mode, execution routes through a CSP-exempt sandbox iframe. The API surface is identical in both modes — scripts do not need to handle mode differences.
DIFFERENCES FROM NODE.JS
require()andimportare not available — there is no module system.- No
node_modulesor npm packages. - The
fsglobal is a VFS bridge, not Node'sfsmodule. Method names and signatures differ. exec()runs commands through the WASM bash interpreter, notchild_process.fetch()is available (browser global).
DISCOVERY DETAILS
- Any
.jshfile anywhere on the VFS is discoverable. /workspace/skills/is scanned first — skill scripts take priority.- Built-in command names shadow
.jshscripts with the same name. - First file found for a given basename wins; duplicates are ignored.
which <command>shows the VFS path for discovered.jshcommands.
SEE ALSO
man skill — skill system that ships .jsh scripts. man commands — full list of available shell commands.