Skip to content

Architecture

Rina edited this page May 12, 2026 · 1 revision

Architecture

Overview

claude-tmux-discord bridges Discord and Claude Code using tmux as the process manager. The key insight: Claude sends replies directly to Discord — the bot never scrapes tmux output.

┌─────────────────────────────────────────────────────────┐
│ Discord Server                                          │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │ #claude-room1│  │ #claude-room2│  │ #claude-room3│  │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘  │
└─────────┼─────────────────┼─────────────────┼───────────┘
          │                 │                 │
          ▼                 ▼                 ▼
┌─────────────────────────────────────────────────────────┐
│ Bot Process (Node.js)                                   │
│  ┌─────────────┐  ┌──────────┐  ┌────────────────────┐ │
│  │ MessageBuffer│  │ Auth/ACL │  │ SessionManager     │ │
│  │ (per channel)│  │          │  │ (tmux lifecycle)   │ │
│  └──────┬──────┘  └──────────┘  └─────────┬──────────┘ │
└─────────┼─────────────────────────────────┼─────────────┘
          │  /enter flushes buffer          │
          ▼                                 ▼
┌─────────────────────────────────────────────────────────┐
│ tmux Sessions                                           │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │ claude-room1 │  │ claude-room2 │  │ claude-room3 │  │
│  │ Claude Code  │  │ Claude Code  │  │ Claude Code  │  │
│  │     ↓        │  │     ↓        │  │     ↓        │  │
│  │ discord-send │  │ discord-send │  │ discord-send │  │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘  │
└─────────┼─────────────────┼─────────────────┼───────────┘
          │                 │                 │
          ▼                 ▼                 ▼
      Discord REST API (messages appear in channels)

Key Design Decisions

No Output Scraping

The bot does not read Claude's output from tmux. Instead, Claude's system prompt instructs it to use the discord-send CLI to send messages. The only capturePane() call is in waitForClaudeReady() — polling for the prompt to confirm Claude has booted.

Serial Promise Chain

Each channel has a SessionState.busy promise chain. All prompts and interactive choices are serialized via .then() callbacks. This prevents concurrent tmux input to the same session, which would corrupt the prompt.

Buffer-Then-Enter

Messages accumulate in a MessageBuffer until /enter flushes them. This enables multi-message composition — users can send text across multiple messages, attach files, and then submit everything as one prompt.

Lazy Session Creation

ensure() creates tmux sessions on demand. If the bot restarts, in-memory state is lost but sessions re-create lazily on the next /enter. Registered channels are repopulated from the database on startup.

Dual Environment Injection

exportEnvToSession() uses both:

  1. tmux set-environment — for future child processes spawned by tmux
  2. Shell export keystrokes — for the current shell session

Both are needed because Claude's working directory may differ from the project root, so discord-send can't find the .env file.

Module Dependency Graph

index.ts
├── config.ts (Zod env validation)
├── logger.ts (Pino)
├── db.ts (Prisma singleton)
├── acl.ts (in-memory cache + DB write-through)
├── commands.ts
│   ├── db.ts
│   ├── acl.ts
│   ├── owner.ts (bot owner detection)
│   ├── session.ts (SessionManager)
│   ├── buffer.ts (MessageBuffer)
│   └── config.ts
└── bot.ts
    ├── auth.ts (authorization check)
    ├── session.ts
    │   ├── tmux.ts (shell wrapper via execa)
    │   ├── db.ts
    │   └── config.ts
    ├── buffer.ts
    │   └── attachments.ts
    ├── commands.ts
    ├── acl.ts
    └── config.ts

Data Flow: Message to Response

  1. Discord message receivedEvents.MessageCreate in bot.ts
  2. Authorizationauthorize() checks: not a bot, not DM, registered channel OR ACL match
  3. Attachment download → files saved to <workspace>/.uploads/<timestamp36>-<name>
  4. Buffer appendbuffer.append(channelId, { text, attachments, authorTag })
  5. Acknowledge → bot reacts with emoji
  6. /enter commandbuffer.flush()sessionMgr.sendPrompt()
  7. Session ensure → create tmux session if needed, inject env, start Claude, wait for
  8. Send texttmux send-keys -l <text> + tmux send-keys Enter
  9. Claude processes → runs discord-send "response" as a shell command
  10. Response appearsdiscord-send hits Discord REST API directly

Workspace Layout

workspace/
└── <channelId>/                    # Per-channel workspace
    ├── .claude-tmux-discord/
    │   └── started                 # Resume marker file
    └── .uploads/
        ├── k3x7f-photo.png        # Downloaded attachments
        └── k3x8a-document.pdf

Clone this wiki locally