| Entity | Trust Level | Rationale |
|---|---|---|
| Main group | Trusted | Private self-chat, admin control |
| Non-main groups | Untrusted | Other users may be malicious |
| Container agents | Sandboxed | Isolated execution environment |
| Incoming messages | User input | Potential prompt injection |
Agents execute in containers (lightweight Linux VMs), providing:
- Process isolation - Container processes cannot affect the host
- Filesystem isolation - Only explicitly mounted directories are visible
- Non-root execution - Runs as unprivileged
nodeuser (uid 1000) - Ephemeral containers - Fresh environment per invocation (
--rm)
This is the primary security boundary. Rather than relying on application-level permission checks, the attack surface is limited by what's mounted.
External Allowlist - Mount permissions stored at ~/.config/parachute-agent/mount-allowlist.json, which is:
- Outside project root
- Never mounted into containers
- Cannot be modified by agents
(Pre-0.1.0 installs auto-migrate from ~/.config/paraclaw/ on first 0.1.0 boot. Compat read drops in 0.2.0.)
Default Blocked Patterns:
.ssh, .gnupg, .aws, .azure, .gcloud, .kube, .docker,
credentials, .env, .netrc, .npmrc, id_rsa, id_ed25519,
private_key, .secret
Protections:
- Symlink resolution before validation (prevents traversal attacks)
- Container path validation (rejects
..and absolute paths) nonMainReadOnlyoption forces read-only for non-main groups
Read-Only Project Root:
The main group's project root is mounted read-only. Writable paths the agent needs (store, group folder, IPC, .claude/) are mounted separately. This prevents the agent from modifying host application code (src/, dist/, package.json, etc.) which would bypass the sandbox entirely on next restart. The store/ directory is mounted read-write so the main agent can access the SQLite database directly.
Each group has isolated Claude sessions at data/sessions/{group}/.claude/:
- Groups cannot see other groups' conversation history
- Session data includes full message history and file contents read
- Prevents cross-group information disclosure
Messages and task operations are verified against group identity:
| Operation | Main Group | Non-Main Group |
|---|---|---|
| Send message to own chat | ✓ | ✓ |
| Send message to other chats | ✓ | ✗ |
| Schedule task for self | ✓ | ✓ |
| Schedule task for others | ✓ | ✗ |
| View all tasks | ✓ | Own only |
| Manage other groups | ✓ | ✗ |
parachute-agent stores third-party credentials (API keys, OAuth tokens) in a local AES-GCM-encrypted store and injects them into per-agent containers as environment variables at spawn time. There is no proxy in the request path — the credential is plaintext inside the container at runtime, the same posture as any standard process environment.
How it works:
- Credentials are written via the
/secretspage in the parachute-agent web UI (or thesecretstable directly). Ciphertext lives in the central DB; the master key is at~/.parachute/agent/master.keyon the host (mode 0600). - When parachute-agent spawns a container, it resolves the secrets assigned to that agent group, decrypts them on the host, and passes them as
-e KEY=VALDocker flags. - Agents see the credentials as ordinary env vars. The host process is the only thing that ever holds the master key — containers cannot decrypt the ciphertext on their own.
Per-agent assignment: Each agent group has its own assignment list. A "sales" group and a "support" group can hold disjoint sets of credentials. Rotation is a write to the secret row; the next container spawn picks up the new value.
NOT Mounted:
- Channel auth sessions (
store/auth/) — host only - Mount allowlist — external, never mounted
- Any credentials matching blocked patterns
.envis shadowed with/dev/nullin the project root mount
| Capability | Main Group | Non-Main Group |
|---|---|---|
| Project root access | /workspace/project (ro) |
None |
| Store (SQLite DB) | /workspace/project/store (rw) |
None |
| Group folder | /workspace/group (rw) |
/workspace/group (rw) |
| Global memory | Implicit via project | /workspace/global (ro) |
| Additional mounts | Configurable | Read-only unless allowed |
| Network access | Unrestricted | Unrestricted |
| MCP tools | All | All |
┌──────────────────────────────────────────────────────────────────┐
│ UNTRUSTED ZONE │
│ Incoming Messages (potentially malicious) │
└────────────────────────────────┬─────────────────────────────────┘
│
▼ Trigger check, input escaping
┌──────────────────────────────────────────────────────────────────┐
│ HOST PROCESS (TRUSTED) │
│ • Message routing │
│ • IPC authorization │
│ • Mount validation (external allowlist) │
│ • Container lifecycle │
│ • Local AES-GCM secret store (decrypts + injects as env vars) │
└────────────────────────────────┬─────────────────────────────────┘
│
▼ Explicit mounts + assigned secrets as env vars
┌──────────────────────────────────────────────────────────────────┐
│ CONTAINER (ISOLATED/SANDBOXED) │
│ • Agent execution │
│ • Bash commands (sandboxed) │
│ • File operations (limited to mounts) │
│ • Assigned credentials available as env vars (plaintext) │
│ • Master key + unassigned credentials never enter the container │
└──────────────────────────────────────────────────────────────────┘
Paraclaw uses pnpm with two supply chain defenses configured in pnpm-workspace.yaml:
minimumReleaseAge: 4320 (3 days). pnpm will refuse to resolve any package version published less than 3 days ago. This defends against typosquatting and compromised maintainer accounts — most malicious publishes are detected and pulled within 72 hours.
Excluding a package from the release age gate (minimumReleaseAgeExclude):
This should be rare. When a zero-day fix or critical dependency requires an immediate update:
- The exclusion must be reviewed and approved by a human maintainer
- The entry must pin the exact version being excluded — never a range or wildcard
minimumReleaseAgeExclude: some-package: "1.2.3" # Approved by @user, 2026-04-14 — CVE-XXXX-YYYY fix
- The exclusion should be removed once the version ages past the threshold (i.e. after 3 days)
- Automated agents (Claude, CI bots) must never add exclusions without human sign-off
onlyBuiltDependencies restricts which packages can execute install/postinstall scripts. Only packages on this list are permitted to run build scripts during pnpm install. Currently allowed:
better-sqlite3— compiles native SQLite bindingsesbuild— downloads platform-specific binaryprotobufjs— generates protobuf bindings (used by Baileys/libsignal)sharp— downloads platform-specific image processing binary
Adding a package to this list requires human approval — build scripts execute arbitrary code with the installing user's permissions.
The .npmrc file contains minReleaseAge=3d as a fallback. The authoritative setting is in pnpm-workspace.yaml, but .npmrc provides defense-in-depth if npm is ever invoked directly (e.g. by a tool that doesn't respect pnpm).