Audience : operators deploying ClawPilot, customers evaluating it for regulated environments, and contributors touching authentication, secret, or signing code paths.
Scope : Community Edition (this repo) plus the integration points it exposes to the Enterprise Edition (
claw-pilot-enterprise) and to the licensing service (claw-pilot-licensing).
This document is intentionally short. Each section points at the file path where the actual logic lives so you can verify claims directly. When this document and the code disagree, the code wins — please open a PR to fix the doc.
| Actor | Capability | Goal | Mitigation summary |
|---|---|---|---|
| Local user (multi-tenant) | Authenticated CE/EE account | Escape their tenant, read other tenants' data | Per-row org_id slot on every user-resource table; capability registry gates Enterprise features |
| LAN attacker | Sniffs unencrypted HTTP between browser and dashboard | Steal session cookie, replay it, impersonate the user | SameSite=Strict cookies; session fixation defense rotates IDs after auth (audit 2026-05 F-1); HTTPS at the reverse proxy in production |
| External attacker | Reaches the public IdP only | CSRF on OIDC callback, open-redirect via redirect_uri, replay state |
PKCE-S256 only; one-shot state with 256-bit entropy; redirect URI exact-match enforced by IdP; return_to allowlisted to relative paths |
| Pre-existing local user | Has a local CE account with email = future OIDC email | Hijack the OIDC user once SSO is connected | Resolver keys on oidc_sub, never on email; new OIDC user gets a -2 suffix instead of taking over the local account |
Out of scope : compromise of the upstream IdP, supply-chain compromise of
oauth4webapi / jose / @fastify/rate-limit, physical access to the host,
side-channel attacks on the underlying Node crypto primitives.
The May 2026 security audit walks each entry above and is archived under
claw-pilot-enterprise/docs/security/2026-05-sso-audit.md. The accompanying
pen-test report for the licensing service lives at
claw-pilot-licensing/docs/security/2026-05-pentest.md.
Every secret read at runtime goes through secretProvider.get(name).
There is no other supported pattern: direct process.env.*_SECRET
access, direct fs.readFileSync on *.pem / *.key paths, and ad-hoc
secret manager calls are blocked at the lint stage.
- Provider interface :
src/core/secrets/index.ts - Default env-backed implementation :
src/core/secrets/providers/env.ts - Discipline rule (lint) :
tools/eslint-plugin-clawpilot-discipline/rules/no-direct-secret-access.js
Adding a new secret consumer :
- Pick a stable name (
MYSERVICE_API_KEY, never the value). - Read it from
await secretProvider.get("MYSERVICE_API_KEY", { audit: true }). - Document it in your subsystem's README and add an entry to the deployment
.envtemplate if relevant.
Never commit secrets, never log them. The Hono request logger is configured
to redact Authorization and X-API-Key headers globally.
All shared-secret HTTP signatures (today: webhook triggers ; soon: A2A peers, dashboard outbound notifications) use the same wire format and the same constant-time compare path :
- Module :
src/core/security/hmac.ts - API :
signPayload(secret, payload, algo='sha256') → "<algo>=<hex>"andverifySignature(secret, payload, header, opts?) → boolean - Header convention :
X-ClawPilot-Signature: sha256=<hex>(compatible with GitHub / Stripe / Slack) - Algorithms :
sha256(default),sha384,sha512— opt-in viaallowedAlgos. A header that announces a stronger algorithm than the caller opted into is rejected before the digest compare, defeating downgrade attempts that flip the prefix without flipping the digest. - Comparison : SHA digests are decoded to
Bufferand compared viacrypto.timingSafeEqual. Any malformed input returnsfalserather than throwing — callers do not need a try/catch.
The only call site today is the inbound webhook verifier
(src/runtime/triggers/webhook-verifier.ts),
which is a thin wrapper around verifySignature. Future surfaces should
do the same — never reinvent the verifier.
- Storage :
users.password_hash, scrypt viacrypto.scryptSync - Login route :
src/dashboard/routes/auth.ts - Rate-limit on
/api/auth/login:loginRateLimiter(in-memory sliding window per IP) - Session storage :
sessionstable keyed by a nanoid cookie, 24 h sliding TTL,SameSite=Strict+HttpOnly
The OIDC routes are mounted by the Enterprise edition on the same Hono app as the local backend. CE-only deployments do not expose them.
- Routes :
claw-pilot-enterprise/src/enterprise/routes/oidc.ts - Flow : Authorization Code with PKCE-S256 (no
plainfallback) - State : 256-bit random, persisted with TTL ≤ 10 minutes, atomic read-then-delete on callback (one-shot, replay-resistant)
- ID token validation : signature +
iss+aud+exp+nbf+iat+nonce, JWKS auto-rotated onkidmiss
The May 2026 audit confirmed the implementation closes the OWASP-style
12-item OIDC checklist. Three findings were patched in the same sprint :
session fixation rotation, complete /logout handling, and a JIT race
window. Details : claw-pilot-enterprise/docs/security/2026-05-sso-audit.md.
The dashboard UI tries the OIDC logout path first
(GET /api/auth/oidc/me → POST /api/auth/oidc/logout if linked) and
falls back to POST /api/auth/logout for password-authenticated or
CE-only users. The OIDC logout :
- Deletes the local dashboard session (always, regardless of provider settings).
- Returns the IDP
end_session_endpointURL whenpropagate_logout=1, so the UI can redirect there (RP-Initiated Logout).
Limitation : Back-channel logout (RFC OIDC BCL) is not supported. A user closing their browser without clicking "Sign out" leaves the IDP session valid. Operators that require this guarantee should configure short IDP session lifetimes.
The Enterprise licence server (claw-pilot-licensing, separate repo) issues
ed25519-signed JWTs that EE consumes via verifyLicence in
claw-pilot-enterprise/src/enterprise/capabilities/licence-verifier.ts.
Public claims expected by EE :
| Claim | Source | Notes |
|---|---|---|
iss |
always clawpilot-licensing |
hard-coded in the verifier |
sub |
customer id | opaque |
aud |
always claw-pilot-enterprise |
introduced 2026-05; legacy licences without aud accepted during a 30-day rotation window |
iat / exp |
unix seconds | 60 s clock tolerance |
licence_id |
UUID | preferred; legacy jti is read as a fallback |
capabilities |
string[] |
preferred; legacy features is read as a fallback |
The licence server hardens its admin surface with :
X-API-Keyconstant-time compare viacrypto.timingSafeEqualover SHA-256 digests- IP allowlist on
/admin/* @fastify/rate-limit(60 req/min global, 10 req/min on/admin/licences)- Stable error codes returned as
{ code, message }(e.g.LICENCE_REVOKED,RATE_LIMIT_EXCEEDED,AUTH_API_KEY_INVALID) audit_eventsledger row on every auth failure
A pen-test driver lives at
claw-pilot-licensing/scripts/pentest-licence.ts (run via pnpm tsx) and
exercises the full set of probes; the most recent report is committed to
docs/security/2026-05-pentest.md in that repo.
Agents can mutate files in their own workspace via ws_write_file and
ws_delete_file. These tools are gated by a per-agent linear scope persisted
in agents.fs_write_scope (schema v42):
| Scope | Tools exposed |
|---|---|
none |
read-only (ws_list_files, ws_search_files) |
own |
+ ws_write_file, ws_delete_file |
own_shared |
+ ws_write_shared_file, ws_delete_shared_file (instance shared workspace) |
system |
reserved for FS-WRITE-001 (fs_write_file, fs_delete_file) |
Every existing agent migrates to none on upgrade — the operator must
explicitly opt in via the agent's Permissions tab (PR2).
The write path enforces, in order:
validateWorkspaceRelativePath— refuses absolute paths,..traversal, reserved segments, non-allowed extensions (src/lib/workspace-path.ts).- Core protected paths —
SOUL.md,IDENTITY.md,AGENTS.mdare hardcoded insrc/runtime/plugin/workspace-knowledge/_protected-paths.tsand cannot be overridden via API or UI, even with scopesystem. - Custom protected globs —
agents.protected_paths_json, layered on top of the core list (extends, never replaces). Globs evaluated viapicomatch({ dot: true, nocase: false }). - Allowed-paths whitelist — when set on
agents.allowed_paths_json, a write must match at least one glob. - Per-file size cap — 1 MB (
MAX_BYTES). - Atomic SQL CAS quota —
agents.write_quota_mb×agents.quota_reset_period(daily/weekly/never). The CAS is a singleUPDATE … WHERE bytes_written_period + ? <= capso concurrent writes cannot overshoot. - Disk + DB upsert — workspace file written then mirrored into
agent_files.
Every attempt — success or refusal — emits an agent.workspace_write audit
event (src/core/audit/events.ts) with outcome ∈ {ok, blocked} and a
machine-readable reason on refusals (protected_path, outside_allowed,
too_large, quota, scope_disabled, invalid_path). No file content nor
secret is persisted in the audit row — only the workspace-relative path.
Permission management exposes three endpoints :
GET /api/instances/:slug/agents/:id/permissionsPATCH /api/instances/:slug/agents/:id/permissionsGET /api/instances/:slug/agents/:id/recent-writes?limit=10
The PATCH validates every glob server-side via picomatch.makeRe and
refuses to mutate the core protected list (it is hardcoded and never read
from the database).
ClawPilot is built and operated by Stephane (Castelis). Single-dev coverage applies — no 24/7 on-call.
- Email : security@castelis.com (preferred)
- Telegram :
@swoelffel(fastest path, business hours CET/CEST) - GitHub : private security advisory on the affected repo
Please give the operator at least 7 days to reproduce and fix before disclosure. Critical vulnerabilities (CVSS ≥ 9.0) targeting reachable production deployments will be acknowledged within 24 hours.
- 2026-05-04 — initial version after the May 2026 security sprint (audit C1, R5 ESLint extension, HMAC canonical module, licence server hardening, OIDC logout follow-up).
- 2026-05-08 — added "Workspace write scopes" section after WS-WRITE-001 PR1 (linear scope ladder, hardcoded core protected paths, atomic CAS quota, audit event taxonomy).