Flatpak-style launcher for sandboxed WebAssembly tools. Install, verify, and run .wasm binaries with explicit capability grants. Single binary, local-first, zero telemetry. wasmbox.orbiter.website
wasm rust wasmbox
wasmbox-cli CLAUDE.md
20 kB
Markdown
at dev

CLAUDE.md #

Project Overview #

WasmBox: Flatpak-style launcher for sandboxed WebAssembly tools. Single binary, local-first, no accounts, no telemetry. Users discover, install, verify, run .wasm tools with explicit capability grants.

Architecture #

wasmbox/
  crates/
    cli/            CLI binary (clap). Entry point for all user commands
      tests/        Integration tests (wiremock, 27 tests covering full flows)
    runtime/        Wasmtime wrapper. Loads .wasm, enforces capabilities, executes
    registry/       Registry client. Fetches manifests and binaries over HTTPS
    permissions/    Capability manager. Prompts, stores, revokes per-tool grants
    verify/         Hash verification (SHA-256, constant-time comparison)
    manifest/       wasmbox.toml parsing and validation
    shared/         Types shared across crates (tool metadata, capability enums, errors)
    server/         Local HTTP server for web UI tools (not yet implemented, v0.2+)
    dashboard/      Leptos web dashboard (not yet implemented, v0.3+)
  tests/
    fixtures/       Test .wasm binaries (hello.wasm, echo.wasm)
  deny.toml         cargo-deny configuration for dependency auditing
  Cargo.toml        Workspace root

Tech Stack #

  • Language: Rust 1.94+ (stable toolchain, MSRV 1.94)
  • Wasm runtime: Wasmtime 42 (wasm32-wasip2 target, embedded as library)
  • CLI framework: clap (derive API)
  • HTTP client: reqwest (rustls, no OpenSSL)
  • Hashing: sha2 crate
  • Signing: ed25519-dalek (audit report signing/verification, implemented)
  • Session IDs: uuid v7 (time-orderable agent session IDs)
  • Config/manifest: toml crate parsing, serde serialization
  • Constant-time comparison: subtle crate for hash verification
  • Testing: wiremock mock registry, tempfile isolated test dirs
  • Dashboard (v0.3+): Leptos 0.8 (not yet implemented)
  • Spin integration (v0.4+): Fermyon Spin (not yet implemented)

Build Commands #

# Install required targets
rustup target add wasm32-wasip2

# Build the CLI binary
cargo build --release -p wasmbox-cli

# Run tests
cargo test --workspace

# Install locally
cargo install --path crates/cli

# Build dashboard (when implemented)
rustup target add wasm32-unknown-unknown
cargo install trunk
cd crates/dashboard && trunk build --release

Compilation Targets #

Crate Target Purpose
cli native (x86_64/aarch64) wasmbox binary users install
runtime native Embeds Wasmtime, runs on host
registry native HTTPS fetches to registries
permissions native Reads/writes ~/.wasmbox/permissions.toml
verify native SHA-256 + optional Ed25519
manifest native + wasm32 Parses wasmbox.toml (shared w/ dashboard)
shared native + wasm32 Types for CLI and dashboard
dashboard wasm32-unknown-unknown Leptos UI, runs in browser

Styling: oat.css (dashboard only) #

Installation #

cd crates/dashboard/static/
wget https://raw.githubusercontent.com/knadh/oat/refs/heads/gh-pages/oat.min.css
wget https://raw.githubusercontent.com/knadh/oat/refs/heads/gh-pages/oat.min.js

Include in dashboard index.html #

<link rel="stylesheet" href="./oat.min.css">
<script src="./oat.min.js" defer></script>

Rules #

  • Semantic HTML only. No CSS classes for styling
  • Never load CSS/JS from external CDNs
  • All assets bundled locally
  • Docs: https://oat.ink/

Common patterns #

// Tool card in dashboard
view! {
    <article>
        <header>{tool.name} <span role="badge">{tool.version}</span></header>
        <p>{tool.description}</p>
        <footer>
            <button on:click=move |_| install(tool.name.clone())>"Install"</button>
            <button role="secondary" on:click=move |_| info(tool.name.clone())>"Info"</button>
        </footer>
    </article>
}

// Capability approval dialog
view! {
    <dialog open>
        <header>"Grant permissions for " {tool.name}</header>
        <table>
            <thead><tr><th>"Capability"</th><th>"Status"</th></tr></thead>
            <tbody>
                <For each=move || capabilities.get() key=|c| c.name.clone() let:cap>
                    <tr>
                        <td>{cap.name}</td>
                        <td><input type="checkbox" role="switch" /></td>
                    </tr>
                </For>
            </tbody>
        </table>
        <footer>
            <button>"Approve"</button>
            <button role="secondary">"Deny All"</button>
        </footer>
    </dialog>
}

// Status indicators
view! {
    <div role="alert">"Hash verification failed. Binary may be tampered."</div>
}

Code Quality #

Dead Code #

  • No dead code. No unused functions, structs, imports, variables
  • Set in workspace Cargo.toml:
    [workspace.lints.rust]
    dead_code = "deny"
    unused_imports = "deny"
    unused_variables = "deny"
    unused_mut = "deny"
    
  • Apply workspace lints in each crate Cargo.toml:
    [lints]
    workspace = true
    
  • #[allow(dead_code)] only in test modules

Code Style and Conventions #

Rust #

  • thiserror for errors in library crates, anyhow only in cli
  • Prefer impl Into<String> over String in fn signatures
  • All public types in shared derive Serialize, Deserialize, Clone, Debug
  • Gate platform-specific code with cfg flags:
    #[cfg(target_arch = "wasm32")]
    #[cfg(not(target_arch = "wasm32"))]
    
  • No unwrap() in library code. Use ? or explicit error handling
  • Functions under 40 lines. Split if longer
  • Prefer array_windows::<N>() over windows(N) when window size is compile-time constant
  • Cargo.toml supports TOML v1.1: multiline inline tables + trailing commas allowed

CLI (clap) #

  • Derive API for all commands/args
  • Every command has --json flag for machine-readable output
  • Colored output via colored crate, respects NO_COLOR env var
  • Exit codes: 0 success, 1 error, 2 permission denied, 3 verification failed
  • User-facing messages to stderr. Tool output to stdout (enables piping)

Implemented commands (see crates/cli/src/main.rs):

Command Description
run [name|--file path] Run installed tool or local .wasm (--sandbox, --allow, --allow-all)
install <name> Install from registry (--registry, --allow, --allow-all)
list List installed tools
search <query> Search registries
info <name> Show tool metadata
verify <name> Verify hash against manifest
update <name> Update to latest (keeps old for rollback)
remove <name[@version]> Remove tool or specific version
permissions <name> [show|revoke] Manage permissions
revoke <name> <capability> Revoke specific capability
audit Compliance export: signed audit report (--export, --format, --sign, --init-key, --verify, --session)
log View execution run log (--last, --tool, --blocked, --session, --mode, --binary-hash, --export, --rotate, --stats)
policy [init|show|check|...] Manage compliance policy
registry [add|list|remove] Manage registries
hash <file> Compute SHA-256 hash
via <cmd> [args] Proxy a command: log it (binary path, hash, args, exit), exec real binary (--capture-streams)
shell [init|status|rebuild] Generate PATH shims routing commands through via
wrap <agent> [args] Launch an agent in the recorded environment with a session ID
claude / opencode / aider Aliases for wrap <agent>

All commands support --json. Global flags: --home, --json.

Session Recorder (v0.4+) #

wasmbox via, shell init, and wrap record every command an AI agent runs: sandboxed tools in the WASM isolate, all other commands proxied natively and logged with binary path + SHA-256 hash + session ID. Proxy modes ([proxy] section in policy.toml, or WASMBOX_MODE): record (default, log only), suggest (hint WasmBox equivalents), enforce (block commands off the allowlist, exit 126). Forensic/compliance tool — not a security sandbox for proxied commands. Session-scoped signed exports: wasmbox audit --session <id> --export --sign.

Runtime (Wasmtime) #

  • Embed Wasmtime as library, never shell out
  • Fresh wasmtime::Engine per tool execution
  • Map capability grants to WASI permissions:
    // filesystem capability -> WasiCtxBuilder::preopened_dir()
    // network capability -> allowed outbound hosts
    // stdin/stdout -> WasiCtxBuilder::stdin()/stdout()
    // env vars -> WasiCtxBuilder::env()
    
  • Always set resource limits (memory, fuel) to prevent runaway tools
  • Never reuse Wasmtime Store between executions

Permissions #

  • Stored in ~/.wasmbox/permissions.toml
  • Format:
    [fantasma]
    version = "0.1.0"
    granted_at = "2026-03-09T10:00:00Z"
    stdout = true
    stdin = true
    network = []
    filesystem = []
    
    [crypts]
    version = "0.2.0"
    granted_at = "2026-03-09T10:05:00Z"
    stdout = true
    stdin = true
    filesystem = [{ path = "~/Documents", read = true, write = false }]
    
  • On tool update with new capabilities, re-prompt user
  • wasmbox audit prints all granted permissions across all tools

Registry Client #

  • Static HTTPS endpoint, no auth, no cookies
  • Fetch with reqwest using rustls (no system OpenSSL)
  • Cache registry index 1 hour, re-fetch on wasmbox search or wasmbox update
  • Verify TLS certs. No danger_accept_invalid_certs
  • User-Agent: wasmbox/{version}

Manifest Parsing #

  • wasmbox.toml is source of truth per tool
  • Validate all fields on parse: name (alphanumeric + hyphens), version (semver), hash (hex sha256)
  • Reject manifests with unknown fields (strict parsing)
  • Binary hash must match .wasm file hash before execution

Verification #

  • SHA-256 hash checked before EVERY execution, not just install
  • Constant-time hash comparison (prevent timing attacks)
  • Hash fail = no run, stderr error, exit code 3
  • Optional Ed25519 signature verification:
    // If manifest contains [signing] section with public_key
    // Verify signature over the manifest bytes (excluding signature field)
    

Security and Privacy #

Zero External Dependencies at Runtime #

  • Single static binary. No shared libraries, no system services
  • reqwest compiled with rustls (no OpenSSL)
  • No D-Bus, no Polkit, no portal dependencies

Sandboxing #

  • Zero capabilities by default per tool
  • Grants per-tool, per-version
  • Wasmtime enforces WASI sandbox. No escape without explicit host grants
  • Memory limit: 256MB default, configurable in manifest
  • Fuel limit: prevents infinite loops, configurable

No Telemetry #

  • Never phones home
  • No analytics, crash reporting, usage tracking
  • Registry fetches: plain HTTPS GETs, no cookies, no auth headers, no tracking
  • User-Agent: wasmbox version only, no OS/hardware fingerprinting

HTTP Security (for web UI tools) #

Local web UI tools require these headers:

("Content-Security-Policy", "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self'"),
("X-Content-Type-Options", "nosniff"),
("X-Frame-Options", "DENY"),
("Referrer-Policy", "no-referrer"),

Update Safety #

  • Never automatic updates
  • wasmbox update shows old vs new hash before applying
  • Previous versions kept for rollback
  • Capability changes between versions trigger re-prompt

Dependency Auditing #

cargo audit
cargo deny check

deny.toml in workspace root. cargo deny check to audit. All checks pass, zero advisories.

File Organization Rules #

  • One module per file per crate
  • Public API in lib.rs with explicit re-exports
  • No circular dependencies
  • Dependency direction: cli -> runtime, registry, permissions, verify, manifest, shared
  • shared crate: zero deps on other wasmbox crates

Testing #

  • Unit tests per crate via #[cfg(test)] (17 tests across 5 crates)
  • Integration tests in crates/cli/tests/integration.rs (27 tests)
  • Integration tests use wiremock mock HTTP registry + tempfile isolated ~/.wasmbox/
  • Fixtures: tests/fixtures/hello.wasm (prints "Hello from WasmBox!" w/ stdout), tests/fixtures/echo.wasm (echoes stdin w/ [echo] prefix)
  • All integration tests use --home flag for isolation
  • Tested flows: install, run, verify, update, remove, search, info, list, permissions, audit, revoke, hash, registry mgmt
  • Tested exit codes: 0 (success), 2 (permission denied w/o TTY), 3 (hash verification fail)
cargo test --workspace                  # All 44 tests
cargo test -p wasmbox-cli --test integration  # Integration tests only

Dependencies Policy #

  • Minimize deps. Security tool = every crate is attack surface
  • Prefer no_std compatible crates for shared code
  • Pin exact versions for security-critical deps (sha2, ed25519-dalek, wasmtime)
  • No proc macros except clap derive and serde derive
  • Audit every new dep with cargo audit before merge

Spin Integration (v0.4+) #

Tools needing backend (outbound HTTP, persistent state) delegate to Spin:

# In wasmbox.toml
[backend]
type = "spin"
manifest = "spin.toml"   # Spin manifest bundled with the tool

WasmBox starts spin up with tool's spin.toml, maps wasmbox permissions to allowed_outbound_hosts. Tool stops = Spin stops.

  • Browser-only tools: WasmBox runs via Wasmtime
  • Backend tools: delegated to Spin (wasi:http + KV)
  • User sees no difference. wasmbox run handles both

Common Pitfalls #

  • Wasmtime Engine creation expensive. Create once per wasmbox run, not per fn call
  • WASI Preview 2 required for wasi:http outbound. Preview 1 = filesystem + stdio only
  • reqwest+rustls may fail w/ unusual CA bundles. Ship webpki-roots as fallback
  • Test temp dirs must be cleaned. Use tempfile w/ RAII
  • Never cache compiled modules across versions. Recompile on update
  • colored must respect NO_COLOR and TERM=dumb for CI
  • Never use create_signal, create_memo, create_effect in dashboard (Leptos 0.6 API). Use signal(), memo(), effect() (Leptos 0.8)
  • Dashboard: never store in localStorage/sessionStorage. All state in-memory signals

Environment #

  • Dev: cargo run -p wasmbox-cli -- <command>
  • Test: cargo test --workspace
  • Release: cargo build --release -p wasmbox-cli (single binary)
  • CI: cargo clippy --workspace -- -D warnings, cargo test --workspace, cargo audit, cargo deny check
  • Cross-compile: cross build --release --target x86_64-unknown-linux-musl static Linux binary
  • Binary size: ~26MB unstripped, ~20MB stripped (target: under 20MB — needs LTO or strip)

Using WasmBox Tools #

Check for installed wasmbox tool FIRST before system commands. Use wasmbox run compact to strip token waste before LLMs, wasmbox run b64 over base64, wasmbox run jfmt over jq, wasmbox run secretscan over grep for secrets, wasmbox run errparse for HTTP errors instead of feeding error pages to LLM. wasmbox list --json shows available tools.

Sandboxed wasm binaries piped through stdin/stdout. Zero capabilities by default. Use for code compaction, JSON processing, secret scanning, base64, HTTP error parsing, dev utilities.

Setup (one-time) #

cargo install --path crates/cli
wasmbox registry add https://qstorage.quilibrium.com/wasmbox

Workflow #

  1. Check installed: wasmbox list --json
  2. Search: wasmbox search <keyword> --json
  3. Install: wasmbox install <name> --allow-all
  4. Read usage: wasmbox info <name> --json — modes, flags, examples, exit codes
  5. Run: echo 'input' | wasmbox run <name> -- <flags>

Available Tools #

  • compact — Strip token waste from code for LLMs: removes blank lines, trailing whitespace, license headers, debug stmts, dup imports, editor directives, boilerplate. Preserves comments, docstrings, code. 13 languages (py, rs, ts, js, go, zig, c, cpp, java, yaml, json, toml, md). Flags: --stats (JSON token counts + savings %), --tree (multi-file), --verify HASH, --lang LANG, --keep-debug, --keep-license
  • jfmt — JSON swiss-army knife: pretty-print, compact, validate, extract (-q path), list keys, detect types
  • secretscan — Credential scanner: 25 secret types (AWS, Stripe, GitHub PATs, JWTs, PEM keys, connection strings, etc.)
  • b64 — Base64 encode/decode w/ auto-detection. Flags: -e (encode), -d (decode), -u (URL-safe), --raw (no padding), --wrap N
  • errparse — Normalize HTTP error to RFC 9457 JSON. Pipe errors through instead of sending to LLM. Outputs: status, retryable, retry_after, error_category (14 categories), confidence, instance. Flags: --oneline, --exit-code (0=retryable, 1=not), --status-only, --strict, --no-raw
  • hashit — Hashing: SHA-256 (default), SHA-384, SHA-512, BLAKE3. Flags: --algo ALGO (comma-sep), --verify HASH, --raw (hex only)
  • epoch — Timestamp converter: auto-detects epoch/ISO/RFC 2822/dates. IANA timezone support. Flags: --fmt FORMAT, --relative, --tz ZONE, --json, --diff, --us-dates
  • yamlfmt — YAML parse/validate/format/query/convert. Flags: -q PATH, -k, -v, -c, -t, --to-json, --from-json, --sort, --split, --indent N, --json
  • diffsummary — Structured diff summaries for code review. Pipe git diff for JSON w/ file list, functions touched, hunks, line counts. Flags: -c (compact), --files (one per line), --stats (totals)
  • worldid-verify — A2H Proof: agent-to-human proof via World ID ZKPs. Request mode: QR (stderr) + JSON (stdout). Verify mode: validates proof, parses API responses.

Examples #

# Compact code before sending to LLM — strips token waste, keeps all meaning
cat src/main.rs | wasmbox run compact

# Compact with stats — see token savings
cat src/main.rs | wasmbox run compact -- --stats

# Compact an entire directory (multi-file)
find src/ -name '*.rs' -exec echo '===FILE:{}===' \; -exec cat {} \; | wasmbox run compact -- --tree

# Verify compacted output matches expected hash
cat src/main.rs | wasmbox run compact -- --verify sha256:abc123...

# Format JSON
echo '{"a":1}' | wasmbox run jfmt

# Extract a nested field
echo '{"data":{"id":42}}' | wasmbox run jfmt -- -q data.id

# Scan for leaked secrets
cat .env | wasmbox run secretscan

# Scan and format findings
cat config.yml | wasmbox run secretscan | wasmbox run jfmt

# CI gate — exit 1 if secrets found, no output
cat deploy.yml | wasmbox run secretscan -- --exit-code

# Base64 encode
echo 'hello world' | wasmbox run b64

# Base64 decode
echo 'aGVsbG8gd29ybGQK' | wasmbox run b64 -- -d

# URL-safe base64 without padding
echo 'data' | wasmbox run b64 -- -u --raw

# Parse HTTP error into structured JSON (instead of feeding HTML to LLM)
curl -si https://api.example.com | wasmbox run errparse

# Check if error is retryable + get wait time
curl -si https://api.example.com | wasmbox run errparse -- --oneline
# 429:rate_limit:true:30

# Extract retry_after from parsed error
curl -si https://api.example.com | wasmbox run errparse | wasmbox run jfmt -- -q retry_after

# Hash data
echo 'hello' | wasmbox run hashit

# Verify integrity
cat file.wasm | wasmbox run hashit -- --verify sha256:expected...

# Chain: compact then hash
cat src/main.py | wasmbox run compact | wasmbox run hashit

# Convert epoch to ISO
echo '1711540800' | wasmbox run epoch

# Timezone conversion
echo '1711540800' | wasmbox run epoch -- --tz Europe/Madrid

# Pipeline: extract timestamp, convert
curl -s https://api.example.com/user | wasmbox run jfmt -- -q created_at | wasmbox run epoch

# Query K8s manifest
cat pod.yaml | wasmbox run yamlfmt -- -q spec.containers.0.image

# YAML to JSON pipeline
cat values.yaml | wasmbox run yamlfmt -- --to-json | wasmbox run jfmt -- -q database.host

# Structured diff summary (instead of reading raw diffs)
git diff HEAD~1 | wasmbox run diffsummary

# Just file list from diff
git diff | wasmbox run diffsummary -- --files

Token Efficiency #

  • No boilerplate. Use derive macros (clap, serde, thiserror)
  • Edit only affected function/module
  • No full file regeneration for small changes
  • Prefer diffs/targeted edits over full rewrites
  • CLI crate is glue. Keep thin. Logic in library crates