Skip to content

Configuration reference

ShellWatch is configured by a single config.yaml. Endpoints, keys, and passkeys are managed dynamically through the Web UI / REST API and live in the SQLite database; OAuth clients and tokens live in Ory Hydra — the config file is only for bootstrap and security.

The schema is validated by Zod at startup. See config.sample.yaml for the canonical example.

Resolution order: CLI arg → SHELLWATCH_CONFIG env var → ./config.yaml.

server:
externalUrl: http://localhost:3000
security:
rpId: localhost
trustedWebauthnOrigins:
- http://localhost:3000
allowedNetworks:
- 127.0.0.1/32
- "::1/128"
hydra:
publicUrl: http://localhost:4444
adminUrl: http://localhost:4445
server:
port: 3000 # default 3000
externalUrl: https://shellwatch.example.com # required — used for OAuth, deep links
trustProxy: # default false
- 10.0.0.0/8
- 172.16.0.0/12
KeyTypeNotes
portnumberHTTP listen port. Default 3000.
externalUrlstring (URL)The URL clients reach the broker on. Used by OAuth discovery (never derived from request headers), the SPA’s default redirect URI, and deep links.
trustProxybool / number / string / string[]Reverse-proxy trust. Use CIDR(s) of your proxy. true lets any client spoof their IP and is unsafe in production. See Reverse proxy.
security:
rpId: shellwatch.example.com
trustedWebauthnOrigins:
- https://shellwatch.example.com
selfRegistrationEnabled: false # default false
allowedNetworks: # default loopback only
- 0.0.0.0/0
- "::/0"
rateLimit:
selfRegister: { max: 5, windowMinutes: 15 }
passkeyRegister: { max: 10, windowMinutes: 15 }
loginOptions: { max: 20, windowMinutes: 15 }
loginVerify: { max: 10, windowMinutes: 15 }
KeyTypeNotes
rpIdstringWebAuthn relying-party ID. Must match the registration domain. Changing this invalidates existing passkeys.
trustedWebauthnOriginsstring[]Allowed origins for WebAuthn ceremonies. Exact match. Include protocol + port.
selfRegistrationEnabledboolAllow /register without admin invite. The first-user bootstrap always works regardless.
allowedNetworksCIDR[]Per-IP allowlist for /mcp and /agent-proxy. Defaults to loopback.
rateLimitobjectPer-IP, per-route limits for auth-adjacent endpoints.

ShellWatch delegates all OAuth2/OIDC to Ory Hydra — a hard runtime dependency. There is no fallback shim; ShellWatch refuses to start without this section. See Docker for standing up Hydra (file SQLite, no separate DB server), and Reverse proxy for the URL layout.

hydra:
publicUrl: https://shellwatch.example.com # must equal Hydra's urls.self.issuer
adminUrl: http://hydra:4445 # trusted network ONLY — never public
spa:
clientId: shellwatch-web # default
# redirectUri: https://shellwatch.example.com/auth/callback # default: ${externalUrl}/auth/callback
introspectionCacheTtlMs: 60000 # default 60s, max 300000; 0 disables
dcr:
allowedScopes: ["mcp", "agent"] # default
# Default is loopback-only (the two patterns below). Setting this REPLACES
# the default, so to keep loopback AND allow a hosted client list them all.
redirectUriPatterns: # default shown
- "^http://(127\\.0\\.0\\.1|localhost)(:\\d+)?(/.*)?$"
- "^http://\\[::1\\](:\\d+)?(/.*)?$"
# To allow Claude.ai / Claude.com as a hosted MCP client, add:
# - "^https://claude\\.ai/api/mcp/auth_callback$"
# - "^https://claude\\.com/api/mcp/auth_callback$"
KeyTypeNotes
publicUrlstring (URL)Hydra’s public issuer. Advertised in discovery, embedded in tokens, reached by the browser SPA, MCP clients, and the agent-client. Must exactly equal Hydra’s urls.self.issuer.
adminUrlstring (URL)Hydra’s admin API — login/consent acceptance, client CRUD, token introspection. Never expose to the internet.
spa.clientIdstringFirst-party public PKCE client the web UI uses. ShellWatch provisions it in Hydra automatically on boot (idempotent). Default shellwatch-web.
spa.redirectUristring (URL)The SPA’s authorization-code redirect URI. Defaults to ${server.externalUrl}/auth/callback.
introspectionCacheTtlMsnumberHow long bearer-introspection results are cached. This caps revocation latency: a revoked/logged-out token keeps working for up to this long. Lower for tighter revocation; 0 introspects every request.
dcr.allowedScopesstring[]Scopes grantable through mediated Dynamic Client Registration (/api/hydra/register). offline_access is always added (refresh tokens); ui is reserved for the SPA and never grantable via DCR. agent is only grantable when agentSocket.proxyEnabled is on.
dcr.redirectUriPatternsstring[] (RegExp)A registering client’s redirect_uri must match one of these. Defaults to loopback only (the agent-client and local MCP tools). Setting the key replaces the default list — to allow a hosted client like Claude.ai, list both the loopback patterns and the hosted callback. Anchor every pattern with ^…$.
agentSocket:
proxyEnabled: true # default false — enables /agent-proxy WebSocket
notifications:
mcp:
debounceMs: 100 # default 100 — output_available debounce window
vapid:
subject: "mailto:admin@example.com"
publicKey: "BEl62iU..."
privateKey: "UGo..."

When omitted, the entire Web Push UI is hidden. Generate keys with npx web-push generate-vapid-keys.

These keys only do anything on first run, when the database is empty.

seedAdminEndpoints:
- label: Dev Box
address: ubuntu@dev.example.com # [user@]host[:port]
- label: Staging
address: deploy@staging.example.com:2222
seedAdminPasskeys:
- credentialId: "base64url..."
publicKeyHex: "cose-public-key-hex"
counter: 0
transports: []
label: "Admin Passkey"
KeyUse
seedAdminEndpointsPre-populate endpoints. Keys are assigned via the UI after discovery.
seedAdminPasskeysPre-load passkeys (typically exported from another instance via Admin → General).

(seedAdminApiKey is gone — API keys no longer exist. All access tokens are minted through the OAuth flow.)

Terminal window
SHELLWATCH_DB=sqlite:./data/shellwatch.db # env var — default

Currently SQLite-only. data/ must be writable.