Owner decision. Typing the note into a brand-new non-Claude tab spent a model turn per tab and, for a spawned worker or a scheduled run, landed with the first real prompt. Claude keeps its hint through the system prompt, and every other CLI is told nothing. The setting goes with the path it gated: sessionIdle, the fresh set, isHintableSession and the session:idle wiring. sendInput loses `fromUser`, which was only ever false for the hint, and the deps test pins that a reply is a person's own input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
14 KiB
Discord bridge
Codeman can post to a private Discord server when a session needs you, give every tab its own thread, and type your thread reply into that tab. It only makes outbound connections, so Codeman is not exposed to the internet and it works when you are away from your network.
Warning
A reply from an allowed Discord account is typed into agents that can run shell commands. Treat that Discord account like a login to the machine: turn on 2FA and use a strong, unique password.
What it does
- Posts when a session asks for permission, asks a question, goes idle or errors. Which of these a CLI can raise depends on the hooks it has: Claude Code sends a turn end (
stop, aninfoevent that posts only when Which events is set to Everything, andidle_prompt, which posts by default after the turn has waited a minute). DeepSeek Harness sends onlystop. Codex, OMP, OpenCode, Gemini, Antigravity, Pi, Grok and Copilot send no turn end, so they reach Discord through an error, a blocked respawn, a stopped crash loop or their owncodeman notify; Codex's permission and question dialogs are also read off its screen. - Posts a finished turn, and an error or blocked alert, as the CLI's whole last message, with its bold, lists and numbered questions kept (a Markdown table, which Discord cannot draw, becomes an aligned code block), split over several posts if it is long. It reads the CLI's transcript where it keeps one (Claude Code, Codex, DeepSeek Harness, OMP) and otherwise splits the screen into turns (OpenCode, Gemini, Antigravity, Pi, Grok, Copilot), which can start mid-message when a long answer has scrolled off. A shell, and a permission or question dialog, show the last lines of the screen instead, without the prompt row and status lines under them.
- Creates one thread per tab in a single parent channel, so a reply always reaches exactly one session.
- Types your reply into that session as a single line followed by Enter. A single digit sent while a permission or question is waiting goes as the key alone, without Enter, because the dialog acts on the keypress.
- Lets a CLI post a deliberate message (for example "press the reset button on the case") with the
codeman notifycommand, which callsPOST /api/sessions/:id/discord-notify. Each tab may post 6 a minute; past that the command says "too many messages", so a looping agent cannot ping your phone without end. - Saves files you attach in a thread into that tab's working folder and tells the CLI the path. A CLI can send a file back with
codeman notify "message" --file <path>. - Tells each CLI about
codeman notify(see Telling the CLIs).
Setup
Do these once, in order. Never paste the bot token into a chat or a session.
1. Create a private server
- In Discord choose Add a Server, Create My Own, For me and my friends.
- Invite nobody and create no invite links.
- Create one text channel, for example
#codeman-cli.
2. Copy the IDs
- Open User Settings, Advanced and turn on Developer Mode.
- Right-click the server, the channel and your own avatar, and choose Copy ID for each. You need the guild ID, the parent channel ID and your user ID.
3. Create the application
- Go to https://discord.com/developers/applications and choose New Application.
- Name it
Codeman.
4. Configure the bot
On the Bot tab:
- Choose Reset Token and copy it. It is shown once.
- Turn Public Bot off, so nobody else can add it. Discord refuses this while the app has a default install link, so first open the Installation tab, set Install Link to None (untick Guild Install and User Install if it will not save) and save, then come back here.
- Leave Requires OAuth2 Code Grant off.
- Under Privileged Gateway Intents turn Message Content Intent on. Leave Server Members Intent and Presence Intent off.
5. Invite the bot with minimal permissions
On OAuth2, URL Generator:
-
Tick the scope
botonly. Do not tickapplications.commandsor anything else. -
Tick only these permissions (Attach Files and Embed Links are optional: without them, files cannot be sent from a CLI and links show no preview):
Permission Why View Channels See the parent channel Send Messages Post in the parent channel Send Messages in Threads Post in each tab's thread Create Public Threads Make a thread per tab Read Message History Read your replies Add Reactions Acknowledge a reply that was typed Attach Files Send a file from a CLI to the thread Embed Links Show previews for links in posts -
Open the generated URL, choose your private server and authorise.
-
Check the Installation tab still shows Install Link: None, so there is no public way to add the bot.
The ticked boxes are the source of truth. As a cross-check, these eight permissions add up to the permissions integer 309237763136.
6. Optional: allow deleting threads
Only needed for onSessionDelete: delete. Give the bot role Manage Threads on the parent channel only, as a channel permission override and not server-wide. Skip this if you are happy to keep or archive threads.
7. Never grant
Administrator, Manage Channels, Manage Roles, Manage Messages, Mention Everyone, or any voice permission.
8. Configure Codeman
- Open Settings, Notifications, then the Discord group.
- Enter the bot token, guild ID, parent channel ID and your user ID in the allowlist.
- Choose the options below and press Send test. A test message should appear in the channel.
The token is stored in ~/.codeman/discord.json with mode 0600. It is never returned by the API, written to logs or sent over SSE.
9. Phone alerts
In the Discord mobile app set the server to All Messages. Discord's own push notification is what reaches you off-network.
Options
| Option | Values | Default | Effect |
|---|---|---|---|
threadCreation |
lazy, onSessionStart |
lazy |
lazy makes a thread on the first notification. onSessionStart makes one per new tab |
onSessionDelete |
keep, archive, delete |
archive |
What happens to the thread when its tab is deleted. delete needs Manage Threads |
staleThreads |
keep, archive, delete |
keep |
What happens to threads this bot made that no tab uses any more, checked after startup and then daily. A deleted tab's thread leaves the map, so this also archives or deletes threads that onSessionDelete kept or archived. Skips threads under an hour old, other people's threads and other channels, does nothing while the thread map is empty, and closes at most 25 per run. delete needs Manage Threads. Use one channel per Codeman |
pingMode |
off, critical, all |
critical |
critical pings you for permission prompts, questions, errors and urgent posts |
scope |
attention, all |
attention |
attention skips the quiet "response complete" event. all sends it too |
startupHint |
on, off | on | Tell Claude about codeman notify in its system prompt at launch |
Telling the CLIs
A CLI only posts deliberately if it knows it can. While the bridge is on:
- Claude (
startupHint, on by default) is launched with--append-system-prompt, so nothing shows in the conversation. This reaches local tabs only: remote (ssh) and Docker launches are built outside the CLI registry, and have nocodemancommand anyway. - Every other CLI is sent nothing: there is no silent way to tell it, and typing the note as a message would cost a model turn per tab. It can still run
codeman notifyif you tell it to.
The text explains codeman notify "message" --urgent. codeman is on the PATH of every session from the installer or an npm install (a git checkout run without the installer has no codeman command) and reads CODEMAN_SESSION_ID and CODEMAN_API_URL, so no other setup is needed. Without a bridge it prints that the bridge is off and exits cleanly. Tabs started before the bridge was enabled are not told; use the skill or CLAUDE.md for those.
Files
- Received: files you attach to a reply from an allowed user are saved as
<working folder>/.codeman-discord/<message id>-<name>(mode 0600) and the one-line reply the CLI sees ends with[attached: <paths>]. At most 5 files of 8 MB each; names are reduced to letters, digits,.,_and-; only Discord's own HTTPS addresses are fetched. Anything skipped is named in the thread. The folder ignores itself (it holds a.gitignoreof*), so nothing in it is committed. Remote (ssh) tabs have no local folder, so no file is saved for them. - Sent:
codeman notify "message" --file <path>uploads a regular file of at most 8 MB. The real path (symlinks followed) must be inside the tab's working folder, which stops an upload of something outside it by mistake. The agent runs as your user, so this is a guard against accidents, not a sandbox. Remote tabs cannot send files. - A downloaded file is untrusted content: only allowed users can send one, but a CLI that opens it is exposed to anything written inside it.
Security model
- Only messages from IDs in
allowedUserIds, in the configured guild, inside a thread Codeman created, are acted on. Everything else is dropped. - A reply is reduced to one line, stripped of control characters and capped in length before it is typed.
- Replies are rate limited per session.
- A Discord outage or a bad token never affects sessions. The bridge backs off, and the last error shows in Settings.
Rotating and revoking
- Rotate: in the Developer Portal choose Bot, Reset Token, paste the new token into Codeman and press Send test.
- Revoke: kick the bot from the server or delete the application, then choose Remove token in Settings. Deleting
~/.codeman/discord.jsonalone does not reach a running bridge, which keeps the token it loaded until Codeman restarts. - If the token is ever pasted somewhere it should not be, reset it straight away.
Troubleshooting
| Symptom | Check |
|---|---|
| Test fails with 401 | The token is wrong or was reset. Paste the current one |
| Test fails with 403 or 50001 | The bot cannot see the parent channel. Check the role and the channel ID |
| Posts arrive but replies do nothing | Message Content Intent is off, or your user ID is not in the allowlist |
| Thread is not deleted | The bot lacks Manage Threads on the parent channel |
| Nothing arrives on the phone | Check the Discord app's notification setting for the server |