Persistent Discord conversation orchestrator for Claude Code.
A single Claude Code session listens to one Discord channel, creates a durable markdown record of every conversation, and dispatches the actual work to background subagents. The listener stays responsive while threads run in parallel.
Contributions welcome. See CONTRIBUTING.md for the PR flow, issue templates, and code style.
git clone https://github.com/vp58/threadkeep.git ~/.threadkeep
cd ~/.threadkeep
python3 -m pip install -r requirements.txt
bash install.sh
The installer is interactive. It will prompt you for your Discord listen channel id, errors channel id, owner user id, timezone, and bot token, then store the token in the macOS Keychain, render the launchd plists, and start the listener tmux session. Full walkthrough in docs/SETUP.md.
Uninstall with bash uninstall.sh.
- You post a message in your configured Discord channel.
- Threadkeep creates a Discord thread off the message and a markdown conversation file on disk.
- A worker subagent is spawned via the Claude Code Agent tool. It does the actual work and replies in the thread.
- You reply in the thread. The worker spawns again with the full prior context loaded.
- The conversation file is the source of truth. A small JSON registry maps Discord thread ids to session ids.
The listener never does work itself. It sets up state and fires the subagent. This is what lets it handle many parallel conversations without stalling.
The Claude Code Discord plugin gives you a great single-channel interface but the session does everything inline. If a conversation takes 5 minutes the listener is dead to other inbound messages for those 5 minutes. Threadkeep solves that by separating listening from working. Each conversation gets a thread, a transcript file, and its own subagent.
- Top-level message creates a thread, then dispatches the work.
- Thread replies are routed to the right conversation by id.
- All conversations stored as markdown with YAML frontmatter. Easy to grep, easy to back up.
- Channel-ownership filter. The bot only acts on messages in the configured listen channel or in threads it created.
- Native Discord Approve and Reject buttons for outbound sends. The optional gateway client and marker watcher daemon let workers gate outbound email and Slack sends behind a Discord tap.
- launchd templates for macOS, systemd templates for Linux.
- Idempotent installer with first-class uninstall.
Pre-release. The original private deployment has been running unattended since 2026-05-21 handling parallel conversations end to end. The public install path (install.sh, uninstall.sh, plist templates, Keychain integration) was added on 2026-05-23 and is freshly tested but has not yet been tried by users other than the original author.
- macOS 26.4 (Tahoe), Python 3.14.3, websockets 16.0
- macOS 15 (Sequoia) is expected to work but is not regularly tested
- macOS 14 (Sonoma) is expected to work but is not regularly tested
- Linux: see the sketch at the bottom of
docs/SETUP.md. The systemd templates are shipped but the install script is macOS-specific.
cx-chat-listener/CLAUDE.mdis the listener identity, auto-loaded by Claude Code because the tmux session is started with cwd set to that subdir. It is the same content asagent/cx-chat.mdand is re-asserted after/compactand/clear(which fixes the identity-loss bug where the listener forgot its protocol after the first compaction).cx-chat-listener/hooks/precompact-identity.shandcx-chat-listener/hooks/userpromptsubmit-anchor.share user-scoped Claude Code hooks that defend identity across compaction and per-message. Seedocs/SETUP.mdstep 6 for the registration block.agent/cx-chat.mdis the canonical reference copy of the listener prompt (same content as the cwd CLAUDE.md). Kept for documentation and for tools that want to read the protocol without resolving discovery rules.conversations/holds the dispatch script and CLI. The dispatch script handles all deterministic state changes per inbound message.discord-gateway/is an optional but recommended companion: a persistent WebSocket client that delivers Discord button presses to a small router, which writes approval markers to disk. The marker watcher then runs out-of-band outbound work.approval/is the worker-facing API for requesting an outbound approval via Discord buttons.hooks/outbound-send-gate-hook.shis a Claude Code PreToolUse hook that refuses outbound gate calls without a verified approval reference.hooks/discord-file-gate.shis a Claude Code PreToolUse hook that restricts file attachments in Discord replies to an explicit allowlist of directory prefixes.launchd/templates/*.plist.templateare rendered by install.sh with__REPO_ROOT__,__HOME__,__PYTHON_BIN__,__LABEL__, and__TMUX_SESSION__substitutions.systemd/templates/*.service.templateare the Linux equivalents.
See docs/ARCHITECTURE.md for the full diagram and the conversation + approval lifecycles.
All configuration lives in one config.toml file or in environment variables prefixed with THREADKEEP_. The repo never reads secrets from the filesystem outside of optional token files you explicitly point at. See .env.example for the full set of options.
The bot token is stored separately in the macOS Keychain (service threadkeep-secret, account discord-bot-token) and never lands on disk in plaintext.
git clone https://github.com/vp58/threadkeep.git
cd threadkeep
python3 -m pip install -r requirements.txt
python3 -m unittest discover -s discord-gateway/tests -t discord-gateway -v
To run the daemons by hand without launchd:
export DISCORD_BOT_TOKEN=...
cp config.example.toml config.toml
# edit config.toml with your channel ids
python3 discord-gateway/client.py &
python3 discord-gateway/marker-watcher.py &
bash cx-launcher.sh # opens Claude Code with Discord plugin in foreground
- Inbound Discord messages are untrusted input. The worker prompt treats them as such.
- The listener filters by channel ownership before dispatching. Messages in channels Threadkeep does not own are ignored.
- The router refuses interactions from any user other than the configured owner.
- Outbound sends require explicit owner approval via Discord buttons or a typed sha confirmation.
- The default install does not enable
--dangerously-skip-permissionsfor the worker. Permission prompts will surface in your Claude Code UI.
See docs/SECURITY.md for the full trust model and disclosure flow.
MIT. See LICENSE.
This is a public extraction of a private orchestrator pattern that proved itself in production handling parallel conversations end to end. The original codename cx-chat is preserved in the listener identity file.