Phantom runs as a persistent process with full computer access. This document covers the security model, authentication, permissions, and hardening.
These are configured automatically by the app manifest.
| Scope | Purpose |
|---|---|
app_mentions:read |
Hear @Phantom mentions in channels |
channels:history |
Read messages in public channels |
channels:read |
See public channel list |
chat:write |
Send messages and replies |
chat:write.public |
Post to public channels without being invited |
groups:history |
Read messages in private channels |
im:history |
Read direct messages |
im:read |
See DM list |
im:write |
Start DM conversations |
reactions:read |
Track feedback reactions (thumbs up/down) |
reactions:write |
Add status reactions (eyes, brain, checkmark) |
| Scope | Purpose |
|---|---|
connections:write |
Socket Mode WebSocket connection |
- app_mentions:read + channels:history - The core interaction model. Users @mention Phantom in a channel, and the bot reads the message context to respond. Without
channels:history, the bot cannot see the conversation thread. - channels:read - Used to verify channel existence and list available channels. Not strictly required for basic operation but needed for the bot to discover which channels it has access to.
- chat:write + chat:write.public - The bot must be able to send messages.
chat:writecovers channels the bot has joined.chat:write.publicallows posting to any public channel by ID without needing to be invited first. Thread replies, progressive updates, and the intro message all require these scopes. - groups:history - Same as
channels:historybut for private channels. Without this, the bot silently fails to respond in private channels. - im:history + im:read + im:write - Direct message support.
im:readlets the bot see its DM list,im:historylets it read DM content, andim:writelets it open new DM conversations (used for the DM-based onboarding option). - reactions:read - Phantom tracks thumbsup/thumbsdown/heart reactions as feedback signals. These feed into the self-evolution pipeline to improve the agent over time.
- reactions:write - The status reaction state machine adds emoji reactions to user messages to show processing state (eyes -> brain -> wrench -> checkmark). Without this, the user has no visual indicator that the bot is working.
| Variable | Required | Purpose |
|---|---|---|
ANTHROPIC_API_KEY |
Yes | Claude Opus 4.6 API access |
SLACK_BOT_TOKEN |
For Slack | Bot user OAuth token (xoxb-) |
SLACK_APP_TOKEN |
For Slack | App-level token for Socket Mode (xapp-) |
SLACK_CHANNEL_ID |
For Slack | Default channel for intro message on first start |
TELEGRAM_BOT_TOKEN |
For Telegram | Telegram bot token from @BotFather |
PORT |
No (default 3100) | HTTP server port |
All MCP requests require a Bearer token. Tokens are hashed with SHA-256 before storage. The raw token is shown once during creation and never stored.
# Create tokens
bun run phantom token create --client claude-code --scope operator
bun run phantom token create --client dashboard --scope read
# Tokens are stored as hashes in config/mcp.yaml| Scope | Access |
|---|---|
read |
Query status, memory, config, metrics, history, list dynamic tools |
operator |
read + ask questions, create tasks |
admin |
operator + register/unregister dynamic tools |
Admin scope is required for dynamic tool registration because dynamic tools can execute arbitrary code.
Token bucket rate limiter per client. Default: 60 requests/minute, burst of 10. Configure in config/mcp.yaml.
The webhook channel uses HMAC-SHA256 signature verification:
- Request body is signed with a shared secret
- Timestamp freshness check (5-minute window) prevents replay attacks
- Timing-safe comparison prevents timing attacks
The agent runtime includes safety hooks:
- Dangerous Command Blocker - blocks destructive commands (
rm -rf /,mkfs,dd to device,docker system prune,git push --force, etc.) before execution. This is defense-in-depth, not a security boundary. The real safety layers are the constitution, LLM judges, and owner access control. - File Tracker - tracks all files the agent reads and writes for audit
The self-evolution engine has 8 immutable principles in phantom-config/constitution.md that cannot be modified by the evolution process:
- Never exfiltrate data
- Never modify its own safety hooks
- Always respect user corrections
- Always maintain audit trail
- (and 4 more)
These are enforced by the Constitution Gate during every evolution cycle. The constitution checker rejects any config delta that would violate these principles.
Every evolution change passes through 5 gates:
- Constitution Gate - does it violate immutable principles?
- Regression Gate - does it break golden test cases?
- Size Gate - is any config file over 200 lines?
- Drift Gate - has semantic distance from the original drifted too far?
- Safety Gate - does it touch protected patterns?
When LLM judges are enabled, triple-judge voting with minority veto is used for safety-critical gates (safety and constitution). A single judge objection blocks the change.
- API keys are provided via environment variables, never stored in config files
config/channels.yamluses${ENV_VAR}substitution so tokens stay in the environment- MCP tokens are stored as SHA-256 hashes, not plaintext
- On Specter VMs, secrets are injected via cloud-init and deleted after boot
phantom initwrites tokens to.env.local(gitignored) when provided interactively, but skips.env.localwhen tokens come from environment variables (avoids duplicating secrets)
When deployed via Specter:
- Dual firewall (Hetzner Cloud Firewall + ufw), ports 22/80/443 only
- Caddy provides automatic TLS via Let's Encrypt
- systemd hardening: NoNewPrivileges, ProtectSystem=strict, ProtectHome=read-only, PrivateTmp
- fail2ban for SSH brute-force protection
- The agent process runs as the
specteruser, not root - Memory limits: 2GB max, 1.5GB high watermark, 256 max tasks
Every MCP interaction is logged in SQLite:
- Client name, method, tool name
- Input summary (truncated to 200 chars)
- Duration, cost, success/error status
- Timestamp
View recent entries via phantom_history MCP tool.
Dynamic tools (registered at runtime by the agent) execute code in isolated subprocesses:
- Only admin-scoped clients can register tools
- Two handler types:
shell(bash commands) andscript(bun scripts on disk) - The
inlinehandler type (which usednew Function(), equivalent to eval) has been removed as a security P0 fix - Subprocesses run with a sanitized environment containing only PATH, HOME, LANG, TERM, and TOOL_INPUT. API keys, tokens, and other secrets are never passed to dynamic tool subprocesses.
- Bun script handlers use
--env-file=to prevent automatic loading of.envfiles - Tool input is passed via the TOOL_INPUT environment variable (JSON string)
Webhook callback URLs are validated before use to prevent SSRF attacks:
- Private IP ranges are blocked (10.x, 172.16-31.x, 192.168.x, 127.x)
- Cloud metadata endpoints are blocked (169.254.169.254, metadata.google.internal)
- Localhost and 0.0.0.0 are blocked
- Only HTTP and HTTPS protocols are allowed
Phantom's security is defense-in-depth, with multiple independent layers:
- Owner access control - Only the configured owner can talk to the agent (Slack user ID filtering)
- Constitution - 8 immutable behavioral principles the agent cannot override
- LLM safety judges - Independent Sonnet 4.6 judges review evolution changes, triple-judge with minority veto
- Dangerous command blocker - Regex patterns catch obvious destructive commands (not a security boundary)
- Subprocess isolation - Dynamic tools run in clean environments without secrets
- MCP authentication - Bearer tokens with scoped access (read/operator/admin)
- Network firewalls - Hetzner Cloud Firewall + ufw, ports 22/80/443 only
- Audit logging - All MCP interactions logged to SQLite