Platter — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Platter (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
<p align="center"> <img src="linux/platter.svg" width="128" height="128" alt="platter icon"> </p>
Your computer, served on a platter.
MCP server that exposes Read, Write, Edit, Bash, Glob, Grep, and JS tools over Stdio and StreamableHTTP transports. Built with Bun, compiles to standalone executables.
Designed to be used by browser-based (or any MCP-compatible) agents, like Hadrian, to control a computer.
| Tool | Description |
|---|---|
| read | Read file contents with pagination (offset/limit). Detects image files (JPEG, PNG, GIF, WebP) and returns metadata. Truncates text to 2000 lines or 50KB. |
| write | Create or overwrite files. Auto-creates parent directories. |
| edit | Find-and-replace with exact or fuzzy matching (normalizes smart quotes, dashes, and Unicode whitespace). Requires a unique match, or use replace_all for every occurrence (exact matches only). Returns a unified diff. |
| bash | Execute shell commands with optional timeout. Output truncated to last 2000 lines or 50KB. |
| glob | Fast file pattern matching. Returns up to 500 paths matching a glob pattern (e.g. **/*.ts). |
| grep | Search file contents using ripgrep. Supports regex, file filtering, context lines, and multiple output modes. Requires rg to be installed. |
| js | Evaluate JavaScript/TypeScript in a persistent Node.js vm context. State persists across calls within a session. Supports await, console.log, and loading packages from unpkg.com via await load("package"). Auto-returns the last expression. Not a security sandbox — see Security below. |
Download the latest binary for your platform from Releases, or grab it with curl:
# Download (replace the filename for your platform)
# Available: platter-linux-x64, platter-linux-arm64, platter-darwin-x64, platter-darwin-arm64
curl -fsSL https://github.com/hadriangateway/platter/releases/latest/download/platter-linux-x64 -o platter
chmod +x platter
./platter # stdio mode
./platter -t http # HTTP mode on :3100docker run --rm -i ghcr.io/hadriangateway/platter # stdio mode
docker run --rm -p 3100:3100 ghcr.io/hadriangateway/platter -t http --host 0.0.0.0 # HTTP modeSee Docker below for mounting paths, networking, installing extra software, and building custom images.
bun install
bun run dev # run directly from TypeScript
bun run compile # build standalone binary for current platformplatter v1.x.x
Your computer, served on a platter.
Usage: platter [options]
Options:
-t, --transport <stdio|http> Transport mode (default: stdio)
--tray Run the HTTP server with a Linux system tray
(implies --transport=http, persists state
across restarts in ~/.config/platter)
-p, --port <number> HTTP port (default: 3100)
--host <address> HTTP bind address (default: 127.0.0.1)
--cwd <path> Working directory for tools (default: current directory)
--cors-origin <origin> Allowed CORS origin (default: *)
--auth <mode> Auth mode: oauth, bearer, jwks, none (default: oauth)
--auth-token <token> Bearer token for HTTP auth (auto-generated if omitted)
--tls-cert <path> TLS certificate file (PEM) — enables HTTPS
--tls-key <path> TLS private key file (PEM)
External JWKS / OIDC (--auth jwks — verify tokens from an external IdP):
--oauth-issuer <url> Issuer URL; OIDC-discovers the JWKS endpoint
--jwks-url <url> Explicit JWKS endpoint (overrides discovery)
--oauth-audience <aud> Expected token audience (strongly recommended)
--jwks-scope-grants Map tools:<name> token scopes to tool access
(fail closed: a token with no tools:<name>
scopes is granted no tools)
Process management:
--max-processes <number> Max concurrent bash processes per session (default: 20)
--max-sessions <number> Max concurrent HTTP sessions (default: unlimited)
Restrictions:
--tools <list> Comma-separated tools to enable (default: all)
Valid: read, write, edit, bash, glob, grep, js
--allow-path <path> Restrict read/write/edit/glob/grep to this path (repeatable)
Does not restrict bash or js
--allow-command <regex> Allow bash commands matching this pattern (repeatable)
Pattern must match the entire command string
Applies to bash only; does not restrict js
Sandbox (applies to bash only; the js tool is never sandboxed):
--sandbox Use just-bash sandbox instead of native bash
--sandbox-fs <mode> Filesystem backend: memory, overlay, readwrite (default: readwrite)
--sandbox-allow-url <url> Allow network access to URL prefix (repeatable)
-h, --help Show this help message
-v, --version Show version numberYou can limit which tools are registered, which filesystem paths file tools can access, and which commands the bash tool can execute.
#### Tool selection
Only register specific tools. Unregistered tools are completely hidden from MCP clients:
platter --tools read,glob,grep # read-only server
platter --tools read,write,edit # no bash/search/js#### Path restrictions
Restrict file-accessing tools (read, write, edit, glob, grep) to one or more directory trees. Paths are resolved to absolute form and symlinks are resolved via realpath to prevent escaping:
platter --allow-path /home/user/project
platter --allow-path /home/user/project --allow-path /tmp#### Command restrictions
Only allow bash commands whose entire command string matches at least one regex pattern:
platter --allow-command "git( .*)?" # git only
platter --allow-command "git( .*)?" --allow-command "npm( .*)?" # git or npm
platter --allow-command "ls( .*)?" --allow-command "cat .*" # ls or catPatterns are anchored: --allow-command "git( .*)?" compiles to ^(?:git( .*)?)$, so git status matches but rm -rf / && git status does not.
#### Combined example
# Locked-down: read-only tools, scoped to one directory
platter --tools read,glob,grep --allow-path /home/user/project
# Full tools, but bash restricted to git/npm, files restricted to project
platter --allow-path ./my-project --allow-command "git( .*)?" --allow-command "npm( .*)?"Active restrictions are logged to stderr at startup.
Controlled by --auth <mode>:
| Mode | Description |
|---|---|
oauth (default) | OAuth 2.1 Authorization Code + PKCE, with a static bearer token as fallback |
bearer | Static bearer token only |
jwks | Resource-server mode: verifies JWT access tokens issued by an external OAuth/OIDC issuer (Auth0, Keycloak, Okta, Entra) via JWKS |
none | No authentication |
#### OAuth 2.1 (--auth oauth)
MCP clients that support RFC 9728 (Protected Resource Metadata) can discover platter's OAuth endpoints automatically and authenticate without manual token copying.
The flow:
/.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource/mcpPOST /register (RFC 7591 dynamic client registration)/authorize/tokenAuthorization: Bearer <access_token>The confirmation code proves that the person approving the request has access to the platter process — a remote attacker who can reach the consent page cannot approve without it. Codes are single-use and expire after 5 minutes, with a maximum of 5 attempts.
Access tokens expire after 1 hour and can be refreshed. Client registrations are persisted to ~/.config/platter/clients.json. A static bearer token is also accepted as a fallback for clients that don't support OAuth.
#### Bearer token (--auth bearer)
A random bearer token is generated at startup and printed to stderr (or stored in the system keyring in tray mode). Every request must include Authorization: Bearer <token>. You can provide your own:
platter -t http --auth bearer --auth-token my-secret-token#### No authentication (--auth none)
Disable authentication entirely (e.g. behind a reverse proxy that handles auth):
platter -t http --auth none#### External JWKS / OIDC (--auth jwks)
In jwks mode platter acts as a pure resource server: it does not mint its own tokens. Instead it verifies JWT access tokens issued by an external OAuth/OIDC authorization server (Auth0, Keycloak, Okta, Microsoft Entra) — validating the signature against the issuer's JWKS, plus the iss, aud, and exp claims. This lets you put platter behind your existing IdP without a reverse proxy.
# Auth0 (root issuer)
platter -t http --auth jwks \
--oauth-issuer https://YOUR_TENANT.auth0.com/ \
--oauth-audience https://platter.example.com/mcp
# Keycloak (realm issuer)
platter -t http --auth jwks \
--oauth-issuer https://kc.example.com/realms/myrealm \
--oauth-audience platter-api
# Skip discovery / point at a JWKS endpoint directly
platter -t http --auth jwks \
--jwks-url https://idp.example.com/.well-known/jwks.json \
--oauth-audience platter-apiAudience validation (strongly recommended). Set --oauth-audience to the API identifier configured in your IdP. If you omit it, platter accepts any valid token from the issuer — including tokens minted for a different resource server (a confused-deputy risk). platter prints a warning at startup when audience validation is disabled.
Discovery. With --oauth-issuer, platter fetches the issuer's /.well-known/openid-configuration (falling back to /.well-known/oauth-authorization-server) at startup to find the jwks_uri and re-advertise the authorization server to RFC 9728-capable MCP clients (at /.well-known/oauth-protected-resource/mcp). Pass --jwks-url to skip discovery, or if the IdP isn't reachable at startup. If discovery fails but --jwks-url is set, token verification still works — only the RFC 9728 auto-advertisement is skipped.
Authorization (what a token can do). By default a verified token gets admin-level access, bounded only by the operator's CLI restrictions (--tools, --allow-path, --allow-command, --sandbox) — the IdP controls who gets in, the CLI flags control what they can do. Pass --jwks-scope-grants to additionally honor tools:<name> scopes in the token (e.g. tools:read tools:bash), which further narrow the granted tools (they can only narrow, never widen the operator's ceiling). The scope claim and Entra-style scp claim are both supported. In this mode access is fail closed: a token carrying no tools:* scope is granted no tools at all, so a token must explicitly request the tools it needs.
No static fallback. Unlike oauth mode, jwks mode accepts only externally-issued JWTs — --auth-token is rejected.
Note:MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URLis unrelated to this mode. It only concerns platter serving its own issuer over http in--auth oauthmode. An externalhttpsissuer needs no such flag. If you point--auth jwksat a plain-httpissuer (e.g. a localhost Keycloak), platter warns and skips RFC 9728 metadata advertisement, but token verification still works.
To serve over HTTPS, provide a PEM-encoded certificate and private key:
platter -t http --tls-cert cert.pem --tls-key key.pemBoth --tls-cert and --tls-key are required together. When provided, the server listens over HTTPS instead of plain HTTP.
To generate a self-signed certificate for development or trusted internal use:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=myserver'
platter -t http --tls-cert cert.pem --tls-key key.pemFor use with Claude Desktop, Cursor, and other MCP clients that spawn a subprocess:
{
"mcpServers": {
"platter": {
"command": "/path/to/platter"
}
}
}For browser-based agents and remote connections:
platter -t http -p 3100The server exposes a single endpoint at /mcp that handles:
POST /mcp - JSON-RPC messages (initialize, tool calls)GET /mcp - SSE notification streamDELETE /mcp - session teardownCORS is enabled for all origins by default (reflects the request Origin). To restrict to a specific origin:
platter -t http --cors-origin https://myapp.example.comSessions are managed via the Mcp-Session-Id header per the StreamableHTTP spec.
The server validates the Host header to prevent DNS rebinding attacks. When --cors-origin is set, the Origin header is also validated server-side (not just via CORS response headers).
Run platter as a persistent background service with a system tray icon:
platter --trayThis implies --transport=http and adds:
~/.config/platter/config.json — auth token, enabled tools, port, host, and working directory survive restarts.#### Linux installer
The linux/install.sh script performs a user-level install (no sudo required):
./linux/install.sh # install
./linux/install.sh --uninstall # removeThis installs:
~/.local/bin/platter — the binary~/.local/share/applications/platter.desktop — desktop launcher entry~/.local/share/icons/hicolor/scalable/apps/platter.svg — application icon~/.config/systemd/user/platter.service — systemd user unit for autostart--tls-cert and --tls-key. Uses Node.js https module with PEM-encoded certificate and key files.--auth oauth, default) - MCP clients authenticate via Authorization Code flow with PKCE (RFC 7636). Supports dynamic client registration (RFC 7591) and token revocation (RFC 7009).--auth jwks) - resource-server mode: platter verifies JWT access tokens from an external issuer (Auth0, Keycloak, Okta, Entra) against the issuer's JWKS, validating signature, iss, aud, and exp. Asymmetric algorithms only (alg: none and HMAC are rejected). See the External JWKS / OIDC section under Authentication for configuration.X-Frame-Options: DENY, Sec-Fetch-Site validation, and Origin header checking.oauth mode and as the sole method in bearer mode. A random 256-bit token is generated at startup unless you provide --auth-token or set --auth none.127.0.0.1, localhost, and ::1; remote binds accept only the specified --host.--cors-origin is set to a specific origin, requests with a mismatched Origin header are actively rejected with 403 (not just filtered by CORS response headers).--tools, --allow-path, and --allow-command are defense-in-depth controls. They raise the bar significantly but are not a sandbox. The limitations below should be understood before relying on them in a threat model.
#### What they do well
realpath() on both the target and each allowed path before comparison, preventing traversal via ../ or symlinked directories. For write targets that don't exist yet, the nearest existing ancestor is resolved instead.&& malicious-command.#### Known limitations and bypasses
--allow-path entirely (e.g. cat /etc/passwd). If you set --allow-path without also setting --allow-command or removing bash from --tools, a warning is printed at startup. For strong file-access control, either disable bash (--tools read,write,edit,glob,grep) or pair --allow-path with a tight --allow-command allowlist.vm context, which Node's own documentation explicitly states is not a security mechanism — untrusted code can escape the context via known techniques. Even without an escape, the runtime exposes fetch (arbitrary network access, including private/loopback addresses), Buffer, and a load() helper that downloads and executes arbitrary code from unpkg.com or any other URL. None of --allow-path, --allow-command, --sandbox, or --sandbox-allow-url apply to the js tool — these flags only affect the file and bash tools. If the js tool is enabled, treat the server as having roughly the same blast radius as unrestricted bash. To disable it: --tools read,write,edit,bash,glob,grep. For strong isolation, run platter inside a container or VM.--allow-command "git( .*)?" block rm && git status (because the full string doesn't match), but a determined attacker could construct commands that the regex matches yet that execute unintended code, for example if an allowed pattern is too broad. Write patterns as narrowly as possible.--allow-path validates the search directory for glob and grep, but results within that directory tree may include symlinks pointing outside it. The content of those symlink targets could be returned in grep output or listed by glob.For stronger isolation, use the just-bash sandbox, a Docker container, or both.
Opt into just-bash, a TypeScript reimplementation of bash with a virtual filesystem, for sandboxed command execution. No native processes are spawned; the shell runs entirely in the Bun runtime.
platter --sandbox # readwrite fs, no network
platter --sandbox --sandbox-fs memory # pure in-memory fs
platter --sandbox --sandbox-fs overlay # reads from disk, writes ephemeral
platter --sandbox --sandbox-allow-url "https://api.example.com" # allow network to prefix#### Filesystem modes
| Mode | Reads | Writes | Use case |
|---|---|---|---|
memory | Virtual only | Virtual only | Maximum isolation, no disk access at all |
readwrite (default) | Real disk | Real disk | Sandboxed execution with real file access |
overlay | Real disk | In-memory (ephemeral) | Explore files without risk of modification |
#### Network access
Network access is disabled by default. Use --sandbox-allow-url (repeatable) to allow access to specific URL prefixes. Private/loopback IPs are always denied.
platter --sandbox --sandbox-allow-url "https://api.github.com" --sandbox-allow-url "https://registry.npmjs.org"#### Interaction with other restrictions
readwrite and overlay modes, each allowed path is mounted into the sandbox. In memory mode, --allow-path is ignored.#### Limitations
git, node, docker, rg, python are not available. Only bash builtins and just-bash's built-in command set work.Running platter inside a Docker container provides OS-level isolation via Linux namespaces and cgroups. The container boundary limits what the bash tool can access. Even unrestricted commands can only reach the filesystems and network that the container exposes.
# Minimal: no host filesystem, no network
docker run --rm -i --network none ghcr.io/hadriangateway/platter
# Read-only project access, no bash
docker run --rm -p 3100:3100 \
-v /home/user/project:/work:ro \
ghcr.io/hadriangateway/platter -t http --host 0.0.0.0 --tools read,glob,grep
# Full tools, scoped to a mounted directory
docker run --rm -p 3100:3100 \
-v /home/user/project:/work \
ghcr.io/hadriangateway/platter -t http --host 0.0.0.0 --allow-path /work#### What the container enforces
-v) are visible. Even with bash enabled, commands cannot read or write host paths that aren't mounted.--network none completely disables networking. Without it, the container has outbound access but no access to host-only services unless --network host is used.--memory, --cpus, and --pids-limit flags can cap resource usage to prevent denial of service.#### Containers vs VMs
Containers share the host kernel; isolation is enforced by kernel features (namespaces, cgroups, seccomp). A kernel vulnerability or a misconfigured container (e.g. --privileged) can break the boundary. VMs run a separate kernel on virtualised hardware, so a guest compromise does not directly expose the host. If your threat model includes untrusted code that may attempt kernel exploits, run platter inside a VM (or a VM-backed container runtime like Kata Containers or Firecracker). For most use cases, like limiting blast radius from an AI agent, a properly configured container (non-root, capabilities dropped, --network none) is sufficient.
#### Combining sandbox and container
The just-bash sandbox and Docker container address different layers. Used together, they provide defense in depth:
| Layer | Protects against |
|---|---|
| just-bash sandbox | Arbitrary native process execution: no git, curl, rm, etc. Commands run in a TypeScript interpreter, not the OS shell. |
| Docker container | Host filesystem/network access: even if the sandbox has a bug or is bypassed, the container limits blast radius to mounted paths and allowed networks. |
# Maximum isolation: sandbox inside a container, overlay fs, no network
docker run --rm -i --network none \
-v /home/user/project:/work:ro \
ghcr.io/hadriangateway/platter --sandbox --sandbox-fs overlay
# Sandbox with controlled network access inside a container
docker run --rm -p 3100:3100 \
-v /home/user/project:/work \
ghcr.io/hadriangateway/platter -t http --host 0.0.0.0 \
--sandbox --sandbox-allow-url "https://api.github.com"For the highest security posture, also run the container as a non-root user (--user), drop all capabilities (--cap-drop ALL), and set the filesystem read-only (--read-only) with a tmpdir for any needed writes:
docker run --rm -p 3100:3100 \
--user 1000:1000 \
--cap-drop ALL \
--read-only --tmpfs /tmp \
-v /home/user/project:/work \
ghcr.io/hadriangateway/platter -t http --host 0.0.0.0 --sandboxSee Docker for full usage instructions including mounting paths, networking, and building custom images.
The Docker image is based on Debian Bookworm (slim) and includes ripgrep. Multi-arch images (linux/amd64, linux/arm64) are published to GitHub Container Registry on every tagged release.
docker pull ghcr.io/hadriangateway/platter # latest release
docker pull ghcr.io/hadriangateway/platter:1.0.0 # specific versionPipe JSON-RPC messages via stdin/stdout:
docker run --rm -i ghcr.io/hadriangateway/platterBind to 0.0.0.0 inside the container so the port is reachable from the host:
docker run --rm -p 3100:3100 ghcr.io/hadriangateway/platter -t http --host 0.0.0.0Mount host directories into the container and use --cwd or --allow-path to give platter access:
# Mount a project directory as the working directory
docker run --rm -p 3100:3100 \
-v /home/user/project:/work \
ghcr.io/hadriangateway/platter -t http --host 0.0.0.0
# Mount read-only
docker run --rm -p 3100:3100 \
-v /home/user/project:/work:ro \
ghcr.io/hadriangateway/platter -t http --host 0.0.0.0 --tools read,glob,grep
# Mount multiple directories with path restrictions
docker run --rm -p 3100:3100 \
-v /home/user/project:/project \
-v /tmp/scratch:/scratch \
ghcr.io/hadriangateway/platter -t http --host 0.0.0.0 \
--cwd /project \
--allow-path /project --allow-path /scratchBy default containers have full outbound network access. You can restrict this with Docker's network options:
# No network access (file-only tools)
docker run --rm --network none -i ghcr.io/hadriangateway/platter
# Access host services (e.g. a local database)
docker run --rm -p 3100:3100 --network host ghcr.io/hadriangateway/platter -t http --host 0.0.0.0The image uses Debian, so you can install packages with apt-get at runtime. This is useful for quick experiments but adds startup latency. For production use, build a custom image instead (see below).
docker run --rm -p 3100:3100 ghcr.io/hadriangateway/platter \
bash -c "apt-get update && apt-get install -y git nodejs && exec platter -t http --host 0.0.0.0"Or interactively:
docker run --rm -it --entrypoint bash ghcr.io/hadriangateway/platter
# inside the container:
apt-get update && apt-get install -y git python3
platter -t http --host 0.0.0.0Layer additional tools on top of the platter image for a ready-to-use environment:
FROM ghcr.io/hadriangateway/platter:latest
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
git \
curl \
python3 \
nodejs \
npm \
&& rm -rf /var/lib/apt/lists/*Build and run:
docker build -t my-platter .
docker run --rm -p 3100:3100 -v ~/project:/work my-platter -t http --host 0.0.0.0docker build -t platter .
docker run --rm -i platterbun install
bun run build # bundle to dist/
bun run compile # standalone binary for current platform -> ./platter
bun run compile:all # cross-compile for linux-x64, linux-arm64, darwin-x64, darwin-arm64
bun run format # format with Biome
bun run format:check # check formatting
bun run lint # lint with Biome
bun run lint:fix # lint and auto-fix with Biome
bun run typecheck # typecheck with TypeScript
bun run test # run testsMIT
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.