The short version
- A Slack channel, or one thread, can mirror one Luminair session. Each turn posts as a thread: the prompt as the parent, the reply inside it.
- The mirror runs on the session owner's Mac over Slack's Socket Mode. No public URL, no server of ours in the path.
- Teammates steer with
@Luminair, replies in the session's threads, and a:ow:reaction. Their session role decides whether a request runs, waits, or is refused. - One early bug: Slack's read methods answered
invalid_argumentsuntil the app sent them form-encoded instead of JSON.
Three days, six commits.
The mirror went from first commit to multiple workspaces, channel creation and an approval switch between 8 and 10 September 2026. Each of those commits carries a Claude co-author line, from three different Claude models. The tests came with them: three test files for the mirror and its workspace registry, 41 test cases between them, all passing when we ran them for this post.
This post was drafted the same way, by a session reading desktop/slack-mirror.js, its wiring in the main process and the git history, with every outside claim checked against the page it links to.
The work was already there.
On 8 December 2025 Anthropic launched Claude Code in Slack as a research preview. TechCrunch's read was that it was a bigger deal than it sounded: “The rollout signals that the next frontier in coding assistants isn't the model; it's the workflow.” SiliconANGLE gave the example of a developer writing “@Claude fix the failing payment tests” in a channel.
In June 2026 Anthropic went further with Claude Tag, which joins Slack “as a team member”. The post says one of its advantages is being multiplayer: “Within a given Slack channel, there's one Claude that interacts with everyone.”
Both run the agent in Anthropic's cloud. Luminair's mirror comes at it from the other side. The session already exists on someone's Mac, with their repository, their engines and their settings. The question was how to let a team see it and nudge it from Slack without moving it anywhere.
No server, on purpose.
Slack can deliver events to an app in two ways. The usual one is a public HTTPS address that Slack calls. The other is Socket Mode: the app opens a WebSocket to Slack and events come down that connection instead, with no public request URL. Slack's docs pitch it at apps behind a corporate firewall.
For a desktop app, that is exactly the right shape. The header comment of the mirror says it plainly: phase one “runs INSIDE the owner's Mac over Slack Socket Mode, so there is no public URL and no new infrastructure: the session only runs while this Mac is up, and so does its mirror.”
The trade-off is honest and visible. The Settings hint ends with: “Keep Luminair running and your computer awake.” If the Mac sleeps, the channel goes quiet. When it wakes, the mirror reconnects on its own, waiting one second after a drop, then doubling the wait up to 30 seconds.
Setup is a Slack app you create in your own workspace. Luminair asks for two tokens: a bot token (xoxb-) and an app-level token (xapp-) with the connections:write scope, which is what opens the socket. Both are kept in the Mac's encrypted storage. Since 9 September you can add several workspaces, each with its own tokens, socket and channel bindings.
One turn, one thread.
When you mirror a session, Luminair posts a status card. For a whole channel it also pins the card and sets the channel topic. Then it listens to the session's live turns. When a turn starts, the app posts the prompt as a new message, prefixed with the name of whoever sent it. When the turn finishes, the reply goes into that message's thread.
working · Alex · 1 queued
Mode: strict (@Luminair, thread replies, :ow:) · requests run right away
Alex: The promo total doubles when a code is applied twice. Find where and add a guard.
The handler never checked whether a code was already applied. Added a guard and a regression test.
Does the same happen with gift cards?
eyesstatusLine, the prompt header from postParent. Names and content are made up. In Slack the card's state shows as a coloured circle emoji; here a filled dot means working and a hollow one idle. Alex typed the prompt in the app, so the Luminair app posts it with his name in front. Sam's thread reply reaches the session because the thread is one the session started; the eyes reaction means it was queued to run, where an hourglass would mean it is waiting for approval.A few details keep the channel readable. Markdown from the engines is converted to Slack's own format, and tables become code blocks because Slack messages have no tables. Long replies are split into pieces under Slack's message limit, at 3,900 characters, breaking on a line where it can. The status card is edited in place, never reposted, and at most once every two seconds.
There is also an echo guard. If a request came from Slack, the mirror remembers it, and when that turn starts it reuses the teammate's own message as the thread parent instead of posting the prompt a second time.
Strict by default.
Posting out is the easy half. Letting a channel send work into someone's session, on their Mac, with their repository, needs rules. The mirror listens for only three things unless the owner opts into more:
- 1A message that mentions @Luminair anywhere in the bound channel.
- 2A reply inside a thread the session itself started.
- 3An owner or editor reacting :ow: to a teammate's message, which turns that message into a request credited to the person who reacted.
A mention can carry a verb: status, stop, ask, unlink, help. It can also carry options before the request: model= to pick a model for that one request, hold to queue it paused, front to jump the line.
Then the sender's identity is checked. The mirror looks the Slack user up, matches their email against the session's sharing roles, and decides.
| Sender | Request | Also allowed |
|---|---|---|
| ●Owner | Runs right away | status, stop, unlink, promote, Run and Remove buttons |
| ●Editor | Runs right away | status, unlink, promote, Run and Remove buttons |
| ○Member | Queued paused, with Run now and Remove buttons | status |
| ×Viewer | Refused: “You have view-only access to this session.” | status |
| ×Not a member | Refused, with the owner's name to ask | status |
| ×Guest or external | Refused before any role check | nothing |
● runs · ○ waits for approval · × refused. Refusals are ephemeral: only the sender sees them.
Two smaller rules matter. The ask verb adds an instruction to answer only and change no files; the help text is careful to call that “instruction-based, not a read-only sandbox”. And duplicate deliveries are dropped: Slack can send the same event twice, so event and message IDs are remembered for ten minutes.
JSON, except when not.
The mirror could create a private channel for a session from the start. On 9 September that flow gained a first step: look up the owner's Slack account by email, so they can be invited into the new channel. On 10 September channel creation failed at exactly that step, with a bare invalid_arguments.
The mirror's small API wrapper sent every method a JSON body. Slack's Web API guide treats JSON as the alternative, not the default: “For write methods that support JSON, you may alternatively send your HTTP POST data as Content-type: application/json.” The write calls worked. The lookup was a read. The fix, in the commit's words, was to “send read methods form-encoded”, and to include Slack's own explanatory messages in the error text so the next failure says more than two words.
// Slack read methods reject a JSON body with a bare "invalid_arguments"; // they only take form encoding. Write methods accept JSON. const FORM_ONLY = new Set(['users.lookupByEmail', 'users.info', 'conversations.list', 'conversations.history', 'apps.connections.open']); const form = FORM_ONLY.has(method); const payload = form ? new URLSearchParams(args).toString() : JSON.stringify(args); // ...and on failure, Slack's response_metadata.messages ride along in the error
To be fair to Slack, the reference page for users.lookupByEmail lists JSON as an accepted content type. The commit does not record why our JSON call failed anyway; it records the fix. We changed the five read methods the mirror uses to form encoding, left the writes on JSON, and added a test.
The same day brought one more channel-creation fix. Some workspaces only let people, not apps, create channels, and the bot's request fails with restricted_action. You can now add an optional user token (xoxp-); Luminair then creates the channel as you and invites the bot into it. Without one, the error says exactly that.
From session to channel.
- 1Open Settings › Connectors › Slack. Paste the bot and app-level tokens from your Slack app, plus the optional user token, and press Add workspace.
- 2Open a session's ⋯ menu and choose Slack mirror….
- 3In Mirror to Slack, pick the workspace, then New private channel or Existing channel, and choose Run right away or Wait for approval. Press Mirror.
- 4In Slack, mention your app followed by
helpfor the list of commands.
The mirror syncs through your Luminair account, so you need to be signed in. A session that is not shared yet is given a private share with no members, just so the mirror has a room to work with. The Slack connector is marked Team in the connectors list.
What a Mac-hosted mirror can't do.
Worth knowing
- The mirror is only as available as the owner's Mac. Asleep or offline means no posts and no requests.
- One channel, or one thread, binds one session. Binding a second session to the same place replaces the first.
- Linking from Slack with a command is not supported yet; binding happens in the app.
- The code has an opt-in “all messages” mode meant for dedicated channels. The Mirror dialog does not offer it, and strict mode is what you get.
- Roles are matched by email. A teammate whose Slack email differs from their Luminair email is treated as not a member.
Sources
- TechCrunch · 8 December 2025Claude Code is coming to Slack, and that's a bigger deal than it sounds
- SiliconANGLE · 8 December 2025Agentic coding comes to Slack as Anthropic launches Claude Code integration
- Anthropic · 23 June 2026Introducing Claude Tag
- Slack developer docsUsing Socket Mode
- Slack developer docsWeb API
- Slack developer docsusers.lookupByEmail
Bring your team into the session
Mirror it into Slack from your own Mac. Nothing to host.