DEV Community

Nekoautomata Miki
Nekoautomata Miki

Posted on

A Discord DM can steer Claude Code—but only if you make the trust boundary explicit

The official Claude Code Discord channel is deliberately conservative: even after a sender is paired or allowlisted, ordinary channel messages arrive through an external-channel boundary rather than becoming indistinguishable from text typed into the terminal.

That default is sensible. Authentication answers who sent this Discord message. It does not automatically answer whether this message should have the same authority as the local Claude Code user.

I wanted a narrower option for one personal setup: let one exact Discord account's direct messages steer the currently active local Claude Code session, while leaving every other sender and every guild message on the normal untrusted path.

I released the result as Claude Code Trusted Discord, an unofficial Linux patcher for the official Discord plugin.

This removes an intentional security boundary. Anyone who controls the configured Discord account can steer the target Claude Code session with the same practical authority as someone typing locally. It is not affiliated with or endorsed by Anthropic or Discord.

Authentication is not authority

A broad implementation would be dangerously easy:

  1. receive a Discord message;
  2. recognize that the sender passed the channel allowlist;
  3. inject the text as trusted user input.

The third step does not follow from the first two. An allowlist may contain collaborators, test accounts, or future pairings. Guild messages also carry a different social and security context from a private owner channel.

The patch therefore adds a second, deliberately small decision boundary:

  • the channel must be a direct message;
  • the sender ID must exactly match one configured numeric Discord account ID;
  • both conditions must be true before trusted delivery is considered;
  • all other messages continue through the upstream external-channel route.

The owner ID lives in a separate file with mode 0600, not in plugin source. The installer does not read or modify the Discord bot token, pairing allowlist, Claude daemon control key, or session transcripts.

Route metadata must not come from message text

A trusted message needs enough context for the session to answer through the right Discord conversation. That metadata should come from Discord.js objects, not from a user-authored pseudo-envelope.

The bridge builds a small local payload from authenticated channel state:

  • source: Discord;
  • direct-message channel ID;
  • Discord message ID;
  • authenticated author ID;
  • platform timestamp;
  • sanitized content;
  • attachment references, when present.

C0 and C1 terminal controls are removed while ordinary newlines and tabs are preserved. More importantly, text inside the message cannot redefine its own sender, channel, or routing identifiers.

This is not a general solution to prompt injection. It is a way to prevent an authenticated owner message from forging the transport metadata used around that prompt.

The surprising constraint: the session must be in the background daemon

The first live version had a misleading failure mode. Trusted delivery worked through Claude Code's local daemon control socket, but an ordinary foreground session attached directly to a terminal was absent from the daemon's worker roster.

Restarting another foreground session did not help. There was no daemon worker to address.

The actual transition is:

/background
Enter fullscreen mode Exit fullscreen mode

Alternatively, launch the session with claude --bg.

After that transition, the exact session appears in Claude Code's background-agent roster and can receive the authenticated local reply operation. You can attach to it later with claude agents or claude attach.

Version 0.1.1 now fails closed with explicit /background guidance when the plugin's session ID is not present in that roster. It does not guess another worker or inject into an arbitrary terminal.

Patch private interfaces defensively

This project depends on two implementation details that can change:

  • the installed TypeScript source of the official Discord plugin;
  • Claude Code's private local daemon protocol.

That makes source drift a security condition, not merely a maintenance inconvenience.

The installer uses exact source anchors and refuses to patch when the expected structure is absent. Before changing a file, it creates a timestamped sibling backup. Reinstalling is idempotent, --dry-run shows what would change, and --uninstall restores the newest clean backup it can identify.

The patch is applied both to active installed-plugin sources and to the official marketplace checkout when present. Otherwise a routine plugin refresh can silently replace the active patch with an unmodified copy.

The test suite covers patch idempotency, drift refusal, protected owner-file permissions, backup behavior, dry runs, uninstall, multi-target preflight, and the background-session error path.

When not to use it

Do not use this bridge when:

  • more than one person controls the trusted Discord account;
  • the bot participates in shared guild workflows where DM identity is not a sufficient boundary;
  • the target Claude session has broader filesystem or service access than you would expose to that Discord account;
  • you need a supported, stable integration contract;
  • you cannot tolerate the patch breaking after a Claude Code or plugin update.

For most setups, the official untrusted channel behavior is the right answer.

This patch is for the narrower case where the Discord account is intentionally treated as a remote owner terminal, the account is protected accordingly, and the operator is willing to re-audit the patch after upstream changes.

Try it with a dry run first

The repository includes installation, uninstall, compatibility, and threat-model notes:

Start by reading the generated patch and running the installer with --dry-run. A trusted remote control path should be something you can inspect, disable, and restore—not a hidden shortcut around the channel boundary.

Top comments (0)