Why does supabase-ssh use just-bash?

HEAD
e58691eMerge pull request #14 from supabase-community/docs/demo-gif
This post might have stale content, as HEAD is 6 commits ahead.

why does this use just-bash? like what's the usefulness of it?

Avatar of anthropic/claude-opus-4.6
Claude Opus 4.6
commented

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 via OverlayFs)
  • Inject virtual files like /supabase/AGENTS.MD and /supabase/SKILL.md (via InMemoryFs)
  • Enforce a read-only filesystem (see ExtendedMountableFs which throws EROFS on 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."

Avatar of julianbenegas
Julian Benegas
commented

how does my coding agent execute commands against supabase ssh?

Avatar of anthropic/claude-opus-4.6
Claude Opus 4.6
commented

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:

  1. SSH connection — Your agent's SSH client connects to the server (ssh.ts:186). The server is built on the ssh2 Node.js library.

  2. 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.

  3. exec event fires (ssh.ts:307) — The SSH protocol has an exec request type where the client sends a single command string. The server receives it here.

  4. Cache check — The server first checks an in-memory CommandCache (command-cache.ts). If the same command was recently run with the same cwd, the cached result is returned immediately. This is a big optimization — common commands like grep -rl 'auth' /supabase/docs/ will hit the cache.

  5. Sandbox creation (ssh.ts:170, bash.ts) — If it's a cache miss, execAndCache() creates a fresh just-bash instance 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: true for extra sandboxing
  6. Command executionbash.exec(command, { cwd: '/supabase', signal }) runs the command inside the emulated bash. An AbortSignal.timeout(10_000) (default 10s) ensures it can't run forever.

  7. 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:

  1. The shell event fires (ssh.ts:364).
  2. A single just-bash instance is created and persisted for the session.
  3. A ShellSession (session.ts) wraps it with readline, a prompt (supabase $ ), tab completion (completion.ts), and a banner with the Supabase logo.
  4. Each line typed is executed via bash.exec() and the result is streamed back. State like cwd persists across commands (so cd docs && ls works).
  5. Typing exit or 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

Why does supabase-ssh use just-bash? — supabase-community/supabase-ssh