OceanCode
Slack
Slack Remote lets you follow and drive your sessions from one Slack channel, from your phone or anywhere else. When a session waits for you, finishes or is blocked, the engine posts it in the channel, and you answer with a button or a reply in the message's thread. The engine dials out to Slack over Socket Mode, so the Mac needs no open port and no relay.
What is posted
One top-level message per event, so each one reaches your phone as a notification. Every message starts
with #<code> · <title> · <folder>: the session's short code, its title and its folder. Nothing is posted
while a turn works.
| Event | Message |
|---|---|
| A permission ask | The permission and its patterns, with the buttons Allow once, Always and Deny. |
| A question | The question and its numbered options. One question with one to five options, one choice and no typed answer gets a button per option; any other ends with Reply in this thread. |
| Done | Done — and the first line of the answer, when a session's turn finishes. |
| Blocked | Blocked — and the reason, when a turn stops on an error, a refusal or a bound. |
An ask raised by a sub-agent is posted for its root session; a sub-agent's finished turn posts nothing.
Once an ask is answered, in Slack or in the window, its message loses its buttons and gains a line saying
how it ended: Allowed once from Slack, Denied, Answered: <text>, Expired after 30 min or
No longer open.
Answering from Slack
Press a button, or reply in the message's thread:
| Reply in the thread | On a permission ask | On a question | With no open ask |
|---|---|---|---|
yes, y, allow, ok | Allow once | The answer | The next prompt |
always | Always allow | The answer | The next prompt |
no, n, deny, reject | Deny | The answer | The next prompt |
stop | Interrupts the session | Interrupts | Interrupts |
| Any other text | Deny, with the text as the correction | The answer | The next prompt |
Case and a trailing . or ! do not matter (Yes. is yes), and a leading mention of the bot is
dropped. A reply in an ask's thread is for that ask only: once the ask is closed, the reply is the next
prompt. A reply in a Done or Blocked thread answers the session's oldest open permission ask, else its
oldest open question, or is the next prompt when it has neither. Top-level messages in the channel, edits
and other bots' messages are ignored: a prompt always goes to a session through one of its threads. A
prompt from Slack shows in the window that has the session open (Ocean pager, Xcode), then its reply, as
if typed there.
Several windows and Macs
Every window that hosts the engine connects: plain oceancode (the pager), oceancode acp and
oceancode serve. One-shot oceancode run, goal and workflow never connect. Slack allows ten
connections per app; a window past that keeps retrying. Automation that launches oceancode acp or
serve unattended sets OC_SLACK=0 (Ocean Factory, Ocean Vibe and the Ocean app do), so it never takes a
press or a reply meant for your windows.
Slack hands each button press or reply to one of the app's connections, whichever window that is. On one
Mac that does not matter: the window that receives it hands it to the window that owns the session,
through peers.db, within about two seconds. A window cannot hand a reply to another Mac, so a reply that
lands on the wrong Mac is dropped. On several Macs, give each Mac its own Slack app; the apps can share
one channel. Name each app after its Mac, so its posts and its /invite are told apart: change
display_information.name and features.bot_user.display_name in the manifest, for
example to Ocean Code – Studio. Each Mac's connected message also names its hostname.
Setting it up
At api.slack.com/apps (opens in a new tab), choose Create New App → From an app manifest, pick your workspace and paste the manifest.
Choose Install to Workspace, then copy the Bot User OAuth Token (
xoxb-…) from OAuth & Permissions.In Basic Information → App-Level Tokens, generate a token with the scope
connections:writeand copy it (xapp-…).Create a private channel and invite the bot by its name:
/invite @Ocean Codewith the manifest as it is, or the name you gave the app. It must be private: whoever can write in the channel can run prompts on this Mac, andconnectrefuses a public channel.Copy the channel's id: open the channel's details; the id (
C…, orG…for an older private channel) is at the bottom.Connect:
oceancode slack connect --channel C0123456789It reads
SLACK_BOT_TOKENandSLACK_APP_TOKENfrom the environment, else two lines piped on stdin (the bot token, then the app-level token), else asks for both in the terminal (what you paste shows on screen). It checks the bot token, that the channel is private, then the app-level token and Socket Mode, postsOcean Code connected from <hostname>(this Mac's hostname) in the channel, and only then stores them and printsSLACK_CONNECT_OK team=<team> channel=<id>.Open a new window, or restart the open ones: a window reads the tokens when it starts.
oceancode slack status shows the team, the bot user, the channel and whether OC_SLACK leaves the bridge
on. connect again without --channel replaces the tokens and keeps the channel; oceancode slack disconnect removes them. Flags and exit codes: 21 · CLI reference.
Ocean pager has no Slack view; status is the surface.
The manifest
display_information:
name: Ocean Code
features:
bot_user:
display_name: Ocean Code
always_online: true
oauth_config:
scopes:
bot:
- chat:write
- groups:history
- groups:read
- channels:history
- channels:read
settings:
event_subscriptions:
bot_events:
- message.groups
- message.channels
interactivity:
is_enabled: true
socket_mode_enabled: truechat:write posts and updates the messages, and always_online shows the bot as present. The groups:
scopes read a private channel and the channels: scopes a public one (the two :read scopes also let
connect tell which the channel is); the two events deliver thread
replies, interactivity delivers button presses, and Socket Mode carries both over the connection the
engine opens, so the app needs no request URL. The app-level token's scope, connections:write, is chosen when you generate the token (step 3); a
manifest cannot carry it.
Privacy
Slack, and everyone in the channel, sees: session titles, folder names, the permissions asked and their
patterns (commands, paths), question text and options, the first line of each answer, and why a turn was
blocked. What you type in a thread goes to the engine and, as a prompt, to the model. Anyone in the
channel can press a button or reply with the same power as you at the keyboard, so connect refuses a
public channel; keep the private one to you and the bot. Each press and reply the engine carries out is
logged with the sender's Slack user id (never the text).
The two tokens and the channel id are in the data dir's auth.json (mode 0600), under slack. No command
prints a token and no log line carries one.
Turning it off
OC_SLACK=0 in a window's environment keeps that window off Slack (02 · Configuration);
oceancode slack disconnect turns it off for every window started afterwards. The webhook recipe in
10 · Hooks is the one-way alternative: it posts, but you cannot
answer.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
not_in_channel | The bot is not in the channel. | /invite @<your bot> in the channel, then connect again. |
channel_not_found | A wrong id, or a private channel the bot is not in. | Check the id (step 5) and invite the bot. |
is a public channel | The channel is public: everyone in the workspace could run prompts on this Mac. | Create a private channel (step 4), invite the bot and connect with its id. |
invalid_auth | A wrong bot token, or the app was uninstalled. | Copy the Bot User OAuth Token again (reinstall first if needed). |
missing_scope | The app lacks a scope from the manifest. | Paste the manifest again, reinstall, and connect with the new bot token. |
Socket Mode refused the app token | A wrong app-level token, one without connections:write, or Socket Mode off. | Generate the token again (step 3); check socket_mode_enabled. |
set both SLACK_BOT_TOKEN and SLACK_APP_TOKEN, only one token on stdin | Only one of the two tokens was given. | Give both: the two variables, or two piped lines (the bot token first). |
A window that loses Slack reconnects on its own; a refused token stops its bridge until the next start.
Its slack: log lines name the bot, the channel and Slack's error (18 · Operations).