Docs · Connect
Connect
A launch you can answer from your phone. The session opens a private Slack channel, says what it is doing there, and takes your replies as if you had typed them at the terminal. For how the pieces fit together, start at the docs.
What it is
selan claude connect is the same launch as selan claude,
with one difference: the session is reachable. It opens a private channel in your
workspace, posts there when it starts, shows what it is working on, and asks its
questions there as well as in the terminal. Answering in Slack answers the session.
Your machine never holds a Slack credential. The workspace token lives at the relay, and the only thing the laptop carries is its own Selan session. Revoking that session cuts the channel with it, and a laptop that never held a bot token cannot leak one.
Selan writes down nothing you say or the model says, though Slack keeps its own copy of the channel under your workspace's retention settings. What Selan persists is which workspace you paired, which channel a piece of work resolved to, and that a session started, so a relaunch can be told from a reconnect. No text, no paths, no filenames.
Starting one
The first run asks you to connect a workspace and prints a link to approve it. After that every launch is one word longer than usual.
selan claude connect
One workspace per account. selan claude disconnect forgets it, so
another can be connected.
The channel a session gets
By default the channel is named after the checkout the launch came from: the repository, the worktree when there is one, and the branch when it is not the default. Launching again from the same place returns to the same channel, so the conversation about a piece of work stays in one place across days and machines.
A session keeps the room it settled into. A websocket is a request and dies on the hour, so a connected session reconnects all day; checking out a branch in the middle of one does not move the conversation. The room is decided once, when the session first connects, and every reconnect is answered with the same one.
Renaming the channel in Slack is safe. Routing is by channel id in both directions. Rename it, move it, call it whatever reads best.
Naming a connection
A checkout is the right name for work on a repository and the wrong one for an agent. A long-running assistant is the same thing whichever directory it happens to be standing in, and it needs to say so.
selan claude connect --cid agent-x
That names the connection rather than the checkout. The channel becomes
#selan-agent-agent-x, and the same name comes back to the same
conversation from any directory and any branch. You go to the agent by name instead
of retracing the directory it started in.
One name, one live connection. Starting a second session on a name another is holding is refused rather than joined: two agents answering in one channel read as one confused conversation, and Slack does not say which machine wrote which line. Once the first ends, the name is free again.
Names are lowercase letters, digits and dashes, the way Slack channels are. A name that is not usable is refused at the terminal rather than quietly reshaped into one that would stop matching later.
Running more than one
Give each agent its own name and each gets its own channel. A message typed in one channel reaches that session and no other, because the channel is the routing key rather than the account. Two agents on one machine never read each other's instructions or report each other's work.
The names are yours: --cid releases, --cid oncall,
--cid inbox-triage. The sidebar becomes a list of what you have running.
Declaring the ones you keep
A name you type every day is a name worth writing down once. Put the launches a
repository has in .selanrc.json at its root, and they come with the
checkout rather than living in somebody's shell history.
{
"agents": {
"reviews": {
"agent": "code-review",
"cid": "reviews"
},
"oncall": {
"agent": "incident-triage",
"cid": "oncall",
"ask": false
}
}
}
selan run reviews
# → selan claude connect --cid reviews --agent code-review
selan run oncall
# → selan claude connect --cid oncall --agent incident-triage \
# --disallowedTools AskUserQuestion
selan run
# lists what the file declares, and launches nothing
The file says which agent and under which name.
.claude/agents/ says what that agent is, so there is no model field
here: the agent definition owns that, and a second place to write it is a second
place for it to disagree.
A cid is what asks for a channel, so an entry without one is an
ordinary launch. Anything you type after the name is kept and kept last, so
selan run reviews --model openrouter:… works and the file stays a set
of names rather than a cage.
A field it does not recognise is refused rather than ignored. A name that quietly is not what it reads like is worse than one that will not start, so a misspelt key says so at the terminal.
"ask": false is for the agent you want to decide rather than
consult you. It silences both ways a session can stop and ask: Claude Code's
own question, and the one that reaches your phone. The second is not disallowed by
name but never offered, because the name a plugin's tools are given is Claude Code's
to decide, and a flag that missed it would leave the file claiming something untrue.
An agent that cannot ask says in its final answer what it could not decide.
A name whose agent does not exist is refused before launching. Both
places Claude Code reads one from are checked, .claude/agents/ beside the
file and the same under your home directory, matched on the frontmatter's
name: the way Claude Code matches it. Without that you would get Claude
Code's list of its own built-in agents, which says nothing about the file that named
yours.
seland run reviews is the same declared launch with permission checks
off. The name expands the same way and then the second binary adds its flag, so you
get that agent, in its own channel, without the prompts. There is no field for this
in the file on purpose. Unlike ask, it widens what the session may do
rather than narrowing how it reports, and that is a decision to make at the terminal
rather than one a committed file makes for everyone.
Shaping what starts
--cid is the only argument connect adds. Everything else
reaches Claude Code as typed, except a --model carrying a provider
prefix, which Selan routes to your company's key. So a connected session can be
shaped the same way any other launch is.
selan claude connect --cid reviews --agent code-review --model claude-opus-5
--agent names a definition in .claude/agents/, the same
files subagents come from, so its model, its tools and its instructions become that
session's. A narrow agent on a fixed name is an assistant that does one job and
always answers in the same channel.
seland connect --cid reviews is the same launch with permission checks
off, for a session you intend to leave running unattended. connect is
read there as it is under selan, so the channel and the name still
happen. It is a separate word rather than a flag so that turning it on is
deliberate. See seland.
Reaching one from outside
A named session can also be reached without Slack and without a terminal: one HTTP request, authenticated by the Selan session token you already have.
curl https://connect.selan.ai/api/v1/messages \
-H "Authorization: Bearer $SELAN_TOKEN" \
-H 'content-type: application/json' \
-d '{"cid":"agent-x","text":"CI is red on main"}'
The message appears in that session's channel and the session acts on it, exactly as if you had typed it there. This is how a CI job, a cron line or another service hands an agent something to do.
A name nobody is holding is refused with 409 rather
than opening a channel. The endpoint reaches an agent, and a message posted into a
room nobody is listening to reads as delivered while going nowhere.
Asking one a question
A message tells an agent to do something. A question waits for its answer, and takes it in a shape you decide.
curl https://connect.selan.ai/api/v1/questions \
-H "Authorization: Bearer $SELAN_TOKEN" \
-H 'content-type: application/json' \
-d '{
"cid": "agent-x",
"question": "Which migrations are pending, and is any of them destructive?",
"schema": {
"type": "object",
"properties": {
"pending": { "type": "array", "items": { "type": "string" } },
"destructive": { "type": "boolean" }
},
"required": ["pending", "destructive"]
},
"timeoutMs": 120000
}'
The request stays open until the agent answers or timeoutMs passes,
and what comes back at 200 is valid against the schema you sent. An
expired deadline answers 504; a name nobody is holding answers
409, and an agent that could not match your schema answers
422 with what it said instead.
The shape is enforced rather than requested. The schema travels to the agent's own machine, where the answer is checked against it before it is sent, and a mismatch goes back to the model as a tool error inside the same turn. It reads what was wrong and corrects.
What is enforced is the shape, not the content. It is still a model's answer, and the same question put to a session that has been doing other work may come back different. If you need the same answer every time, a session carrying its own context is the wrong place to ask.
One question per session at a time. A session runs one turn, so a second would be answered out of the same turn as the first with nothing to say which answer belonged to which.
Which agents are up
The same token lists what you have running, and what each one is doing.
curl https://connect.selan.ai/api/v1/sessions \
-H "Authorization: Bearer $SELAN_TOKEN"
{
"sessions": [
{ "cid": "agent-x", "roomId": "C0A1B2C3D4", "label": "demo · main", "state": "working",
"lastActiveAt": "2026-09-19T10:14:02.000Z", "lastMessageAt": "2026-09-19T10:13:58.000Z" }
]
}
state is working while the agent is narrating and
idle when it is not. lastActiveAt is when it last did
something itself; lastMessageAt is when something last reached it. A
recent message against an older action is exactly "somebody wrote to it and it has
not moved since".
GET /api/v1/sessions/agent-x answers for one name, and answers
200 either way: asking whether an agent is up is a question, and "no"
is an answer to it rather than an error.
Sessions started without a name are not listed. They cannot be reached through the API at all, so offering them here would be a handle that does not exist.
The full reference, including every status code and the browsable API explorer, is generated from the code that serves it.
What it does not do
It does not open a port on your machine. The session dials out; nothing dials in.
It does not give Slack your model access, your files or your shell. It carries sentences to a session you started and answers back.
It does not survive the machine going to sleep. The session is a process on your computer; Slack is a way to talk to it, not a place it lives.