Why does supabase-ssh use just-bash?
why does this use just-bash? like what's the usefulness of it?
Now I have a clear picture. Here's why supabase-ssh uses just-bash:
##The Core Problem
This project (supabase.sh) lets coding agents SSH in and run shell commands (grep, cat, find, ls, etc.) against Supabase documentation mounted as markdown files. That means untrusted users/agents are sending arbitrary shell commands to be executed. Running those on a real OS shell would be a massive security risk.
##Why just-bash
just-bash is a fully emulated bash shell that runs entirely inside the Node.js runtime — no real OS shell is ever invoked. This gives the project three critical things:
###1. Sandboxing / Security
The biggest reason. As you can see in bash.ts, it's configured with defenseInDepth: true and strict executionLimits (max 1000 commands, 1000 loop iterations, 1MB output, etc.). The attacks.test.ts file tests for fork bombs, infinite loops, output flooding, string amplification bombs, path traversal, command injection, and more — all safely contained because nothing ever touches the real OS. A real shell would need containerization (Docker, nsjail, etc.) to get anywhere close to this level of safety.
###2. Virtual Filesystem (VFS)
just-bash provides InMemoryFs, MountableFs, and OverlayFs — virtual filesystem primitives. The project uses these to:
- Mount the Supabase docs directory at
/supabase/docs(read-only viaOverlayFs) - Inject virtual files like
/supabase/AGENTS.MDand/supabase/SKILL.md(viaInMemoryFs) - Enforce a read-only filesystem (see
ExtendedMountableFswhich throwsEROFSon any write attempt) - Observe which files/dirs are read (via
startObservingReads()/stopObservingReads())
There's no need to set up chroots, bind mounts, or containers — the entire filesystem is virtual and constructed in memory.
###3. No Infrastructure Overhead
Because everything is emulated in-process, each SSH connection just creates a new Bash({...}) instance in Node.js. There's no need to spin up containers, VMs, or isolated shell processes per connection. This makes it lightweight and easy to deploy — it's just a Node.js app.
###In Summary
just-bash lets this project offer a realistic bash experience (agents can use grep, find, cat, head, tail, etc. just like they would on a real machine) while being:
- Completely sandboxed — no real shell, no real filesystem, no escape
- Resource-bounded — execution limits prevent abuse
- Lightweight — no containers or VMs, just a JS object per session
- Read-only enforced — at the filesystem level, not just by convention
It's essentially the perfect tool for the use case of "let untrusted agents run bash commands against a curated set of files."
how does my coding agent execute commands against supabase ssh?
Here's the full picture of how your coding agent executes commands against supabase-ssh. There are two modes and two protocols:
##Mode 1: One-shot exec (what most agents use)
This is the most common path. Your agent runs a single command like:
ssh supabase.sh grep -rl 'auth' /supabase/docs/
Here's what happens step by step:
-
SSH connection — Your agent's SSH client connects to the server (
ssh.ts:186). The server is built on thessh2Node.js library. -
Auth & rate limiting — During the authentication phase (
ssh.ts:268), three checks run:- Capacity check — probabilistic rejection if the server is above its soft connection limit (default 80), hard reject at 100.
- Per-IP concurrency — max 10 concurrent connections from the same IP.
- Redis-backed rate limit — sliding window of 30 connections per 60s per IP (via Upstash Redis).
All auth is accepted (no passwords/keys required) — it's a public service.
-
execevent fires (ssh.ts:307) — The SSH protocol has anexecrequest type where the client sends a single command string. The server receives it here. -
Cache check — The server first checks an in-memory
CommandCache(command-cache.ts). If the same command was recently run with the samecwd, the cached result is returned immediately. This is a big optimization — common commands likegrep -rl 'auth' /supabase/docs/will hit the cache. -
Sandbox creation (
ssh.ts:170,bash.ts) — If it's a cache miss,execAndCache()creates a freshjust-bashinstance with:- A virtual filesystem mounting the Supabase docs at
/supabase/docs(read-only) - Virtual files like
/supabase/AGENTS.MD,/supabase/SKILL.MD - Execution limits (max 1000 commands, 1000 loop iterations, 1MB output, etc.)
defenseInDepth: truefor extra sandboxing
- A virtual filesystem mounting the Supabase docs at
-
Command execution —
bash.exec(command, { cwd: '/supabase', signal })runs the command inside the emulated bash. AnAbortSignal.timeout(10_000)(default 10s) ensures it can't run forever. -
Response — stdout is written to the SSH channel, stderr to the stderr sub-channel, and
channel.exit(exitCode)sends the exit code. Then the channel closes. Your agent gets back exactly what it would from a normal SSH exec — stdout, stderr, and an exit code.
##Mode 2: Interactive shell session
If you just run ssh supabase.sh with no command (or your agent opens a shell session), you get an interactive REPL:
- The
shellevent fires (ssh.ts:364). - A single
just-bashinstance is created and persisted for the session. - A
ShellSession(session.ts) wraps it with readline, a prompt (supabase $), tab completion (completion.ts), and a banner with the Supabase logo. - Each line typed is executed via
bash.exec()and the result is streamed back. State likecwdpersists across commands (socd docs && lsworks). - Typing
exitor Ctrl+D ends the session.
##Mode 3: HTTP API (POST /api/exec)
There's also an HTTP endpoint (api.ts) that does the same thing over HTTP:
curl -X POST https://supabase.sh/api/exec \
-H 'Content-Type: application/json' \
-d '{"command": "grep -rl auth /supabase/docs/"}'
This follows the same flow: rate limit → cache check → create bash sandbox → execute → return JSON {stdout, stderr, exitCode}. It's gated behind an ENABLE_EXEC_API=true env var.
##The key architectural point
Every exec request creates a brand new, isolated bash sandbox. There's no shared state between exec calls — each one gets its own virtual filesystem and bash instance. This is both a security feature (no cross-session leakage) and what makes caching safe (same input always produces same output since the docs are static).
Interactive shell sessions, by contrast, reuse a single bash instance so state like cd persists, which is what you'd expect from a shell.
END OF POST