Phantom communicates through pluggable channel adapters. Each channel implements a standard interface, and the agent does not care where messages originate.
The primary channel. Uses Socket Mode (no public URL required).
The fastest way to set up Slack. The included manifest configures all scopes, events, and bot settings in one step.
- Go to api.slack.com/apps > Create New App > From an app manifest
- Select your workspace, switch to the YAML tab
- Paste the contents of
slack-app-manifest.yamlfrom the repo root - Click Create
- Click Install to Workspace and approve the permissions
- Copy the Bot Token (
xoxb-) from OAuth & Permissions in the sidebar - Go to Basic Information > App-Level Tokens > Generate Token and Scopes
- Name it anything (e.g., "socket"), add the
connections:writescope, click Generate - Copy the App Token (
xapp-) - Get your Channel ID: in Slack, right-click the target channel > View channel details > scroll to the bottom
All of these are configured by the manifest. If you are setting up manually, add them under OAuth & Permissions > Bot Token Scopes.
| 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) |
Created under Basic Information > App-Level Tokens.
| Scope | Purpose |
|---|---|
connections:write |
Socket Mode WebSocket connection |
Configured by the manifest under Event Subscriptions > Subscribe to bot events:
| Event | Purpose |
|---|---|
app_mention |
When someone @mentions the bot in a channel |
message.channels |
Messages in public channels the bot is in |
message.groups |
Messages in private channels the bot is in |
message.im |
Direct messages to the bot |
reaction_added |
When someone reacts to a message |
In config/channels.yaml:
slack:
enabled: true
bot_token: ${SLACK_BOT_TOKEN}
app_token: ${SLACK_APP_TOKEN}
default_channel_id: C04ABC123Set the tokens as environment variables (in .env.local or your shell):
export SLACK_BOT_TOKEN=xoxb-...
export SLACK_APP_TOKEN=xapp-...
export SLACK_CHANNEL_ID=C04ABC123With the chat:write.public scope (included in the manifest), Phantom can post to any public channel by channel ID without being invited. You do NOT need to /invite @Phantom for public channels.
For private channels, the bot must be invited with /invite @Phantom before it can read or write messages.
- Thread replies - all responses are posted as thread replies to the original message
- Status reactions - emoji reactions cycle through processing states:
- 👀 (queued) -> 🧠 (thinking) -> 🔧 (using tools) -> ✅ (done)
- 💻 for code-related tools, 🌐 for web tools
⚠️ on errors, ⏳ / ❗ on stalls
- Progressive message updates - "Working on it..." messages update with tool activity in real time
- Feedback buttons - [Helpful] [Not helpful] [Could be better] after every response
- Reaction feedback - thumbsup/thumbsdown reactions feed into the evolution pipeline
- Proactive intro - on first start with
default_channel_idset, Phantom introduces itself
Bot interface via long polling. No public URL required.
- Message @BotFather on Telegram
- Create a new bot with
/newbot - Save the bot token
telegram:
enabled: true
bot_token: ${TELEGRAM_BOT_TOKEN}- Inline keyboard buttons
- Persistent typing indicator (re-fires every 4s to stay active)
- Message editing for progressive updates
- MarkdownV2 formatting with code block preservation
- Commands:
/start,/status,/help
IMAP/SMTP with push notifications via IDLE.
Use any email provider that supports IMAP and SMTP. Gmail, Fastmail, and custom mail servers work.
email:
enabled: true
imap:
host: imap.gmail.com
port: 993
user: phantom@example.com
pass: ${EMAIL_PASSWORD}
tls: true
smtp:
host: smtp.gmail.com
port: 587
user: phantom@example.com
pass: ${EMAIL_PASSWORD}
tls: false
from_address: phantom@example.com
from_name: Phantom- IMAP IDLE for push notifications on new emails
- HTML email responses with clean formatting
- Email threading via In-Reply-To and References headers
- Auto-reply detection (out of office, delivery notifications)
- Code block formatting with monospace font
Generic HTTP endpoint for programmatic integration.
webhook:
enabled: true
secret: ${WEBHOOK_SECRET}
sync_timeout_ms: 25000# Synchronous (wait for response)
curl -X POST https://your-phantom/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: sha256=..." \
-H "X-Webhook-Timestamp: 1711375200" \
-d '{"text": "What is the status of the deploy?", "sender_id": "ci-bot"}'
# Asynchronous (immediate 202, callback later)
curl -X POST https://your-phantom/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: sha256=..." \
-d '{"text": "Run the test suite", "sender_id": "ci-bot", "callback_url": "https://my-server/callback"}'HMAC-SHA256 signature verification with timing-safe comparison. 5-minute timestamp freshness window.
Local terminal interface for development. Auto-enabled when no Slack or Telegram is configured.
bun run phantom start
# Type messages directly in the terminalAll channels implement the same interface:
interface Channel {
id: string;
connect(): Promise<void>;
disconnect(): Promise<void>;
send(conversationId: string, message: OutboundMessage): Promise<void>;
onMessage(handler: MessageHandler): void;
isConnected(): boolean;
}Adding a new channel means implementing this interface and registering it with the channel router.