Skip to content

Connect to other tools

Apps do not talk to each other directly. Each app talks to the hub page through the SDK, and the hub passes the message on. An app connects to other tools in three ways:

Way Direction Example
Topics One app to many Kanban tells every app which session is open
Actions One app to the one app that handles it Kanban asks Cost to show the cost of a session
Capabilities One app to the hub Kanban gives the hub the project list

The connection is between pages only. v1 has no channel between app servers.

A topic is a fact that any number of apps can receive. Each topic is sticky: the hub keeps its last value and sends it to an app when the app connects. The hub publishes two topics itself:

Topic Payload
project.changed {project, encoded, name} when the user picks a project, or null when the user clears it
theme.changed {theme, colorTheme, vars?}. Use bindTheme in place of a subscription

Apps publish the other topics. For the topics of the built-in tools, such as Kanban’s session.changed, see The built-in tools.

  • project is an absolute path. encoded is the form that Claude Code uses for its folder names under projects/. Use it as it is; do not encode a path yourself.
  • The hub sends project.changed and theme.changed to every app at once. It sends an app topic only to the app on screen; a hidden app gets the last value of each topic it missed when it comes on screen.
const hub = ClaudeHub.connect();
hub.subscribe('project.changed', (p) => showProject(p?.project ?? null));
hub.subscribe('session.changed', (s) => {
if (!s) return clearSession();
showSession(s.sessionId, s.projectName, s.gitBranch);
});

Subscribe at start, before the page’s load event.

  1. List the topic in the manifest: "publishes": ["board.selected"]. The name is <noun>.<verb> in lowercase, with your app id as the noun when no other app uses it.

  2. Publish it:

    hub.publish('board.selected', { sessionId, project });

The hub drops a topic that is not in the manifest, and does not send an event back to the app that published it. When the payload has a string project, the hub adds encoded and projectName. Keep the payload small: ids and names. The receiver fetches the rest.

An action is a request that exactly one app handles. The caller names the action and does not know which app handles it. The hub switches to the handler’s tab and hands it the call.

For the actions of the built-in tools, session.cost, project.plugins and project.memory, see The built-in tools.

if (hub.can('session.cost')) costButton.hidden = false;
costButton.onclick = async () => {
const r = await hub.invoke('session.cost', { session: sessionId });
if (!r.ok) console.warn('session.cost', r.reason);
};

Show the control only when can is true: the handler can be turned off. The result comes when the hub has routed the call, not when the handler has finished.

When the app runs alone, give connect a standalone map so the same call opens the other tool in a new tab:

const hub = ClaudeHub.connect({
standalone: { 'session.cost': (p) => p.session && `http://localhost:3543/?view=detail&session=${p.session}` },
});
  1. Declare it in the manifest:

    "actions": {
    "handles": {
    "board.open": { "params": { "session": "string" }, "url": "?session={session}", "mode": "message" }
    }
    }
  2. With mode: "message", handle it in the page:

    hub.handle('board.open', ({ session }) => openSession(session));
hub.invoke()action, paramsHandled?by an enabled appParams valid?types in the manifestSwitch to the handler's tabthe caller gets ok: trueunhandledok: falsebad-paramsok: falseyesyesnonoPage live?and mode: "message"hub:actionto hub.handle() in the pageyesURL complete?and closeGuard is offnoLoad the URLparams filled in, the app reloadsyesOnly the tab switchno
What the hub does with hub.invoke()

When the page is live, the hub sends the call to handle. When it is not, the hub loads the url with the params filled in, so the app must also read them from its query string. With mode: "url", the hub loads the URL each time. When a param of the URL is missing, or the handler has closeGuard on, the hub only switches to the tab, because loading the URL reloads the app. The caller gets {ok: true} as soon as the hub has routed the call, before the handler has finished.

To take over a built-in action, declare the same name and put your app before the built-in tool, or turn the built-in tool off. The hub logs the conflict.

A capability is a job for the hub that one app does. See Capabilities for the fields.

  • projects. The provider answers GET <path> with [{path, modifiedAt}]. The hub calls it for the project palette.
  • terminal. The provider gets CCK_TERMINAL and CCK_TERMINAL_TOKEN in its environment, the token in its iframe URL as #t=<token>, and answers on hub.terminalToken(). It runs the terminal itself. Its liveWork route answers {sessions: [...]}, and the hub keeps the app’s processes alive while the list is not empty.

The first enabled app in tab order that declares a capability is its provider. Today Kanban provides both. See The built-in tools.

The SDK forwards the hub’s keys from your page: Ctrl+Alt+P, Ctrl+Alt+W, Ctrl+Alt+A, Ctrl+Alt+Left, Ctrl+Alt+Right and Alt+1 to Alt+N, one for each of the first nine tabs. Every other key stays in your app. Do not use the hub’s keys for your own commands. See Keyboard shortcuts.