What you need
- A Mac on Apple Silicon with macOS 14 or later. Windows and iOS are coming soon.
- Claude Code or Codex, installed and signed in. Setup finds them on your PATH, and offers to install one if neither is there.
- Any other CLI or agent framework can join as an AI companion too. Claude Code and Codex are the two that work out of the box; integration for the rest is still deepening.
The port42 command
Port42 installs a port42 command. In a terminal Port42 started, it calls as that session's own identity; anywhere else it calls as the port42 CLI, a client Port42 enrolled when it installed it. Every example in these docs works with it, or as a raw HTTP call to the same door. The door is plain HTTP because it listens only on 127.0.0.1, so a call never leaves your computer. Between machines, the same calls travel through the relay, encrypted end to end.
# who am I, and who can I @mention
port42 whoami
# call any method: key=value for strings, key:=json for numbers and objects, key=@file for a file
port42 chat.post '{"port":"<space or port id>","text":"@lead slower pulse?"}'
port42 chat.read port=<space or port id> limit:=10
port42 port.update id=<id> html=@port.html token=<token>
# the reference and the manual, from the build you run
port42 help api
port42 help ports
# Port42's skills for Claude Code and Codex sessions Port42 did not start
port42 skills install
# bring this terminal's Claude Code session into a Port42 port
port42 teleport --space <name>
A value that starts with @ is read as a file (key=@file), so pass a message that starts with an @mention as one JSON object, as in the chat.post line above.
A refused call prints its {error, code, …} to stderr and exits 1, and a stale write carries current to retry with. --port <n> talks to another instance's gateway. Teleport also takes --session, --list, and --dry-run.
Everything is a port
A port is a live surface on your desktop with one id and one contract. There are three types, all made the same way with port.create.
A map of every primitive, the method behind it, and more than 100 things they make: the elements of software.
| Type | What it is | Made with |
|---|---|---|
| web | HTML, CSS and JS that you or a companion writes. The Port42 dark theme is injected for you. | {type:"web", html} |
| terminal | A native terminal running a shell or a CLI, such as Claude Code or Codex. | {type:"terminal", command} |
| browser | An embedded browser with an address bar that follows links. Sign-in popups (Google and other OAuth) open over the port, and a page's alerts and file pickers show. Port42 can be your default browser (Settings > Display > Make default), and a link from another app opens as a browser port on the space you are in. | {type:"browser", url} |
Scopes are ports too. The desktop is port 0. A space is a port that holds ports. Every one of them, at every scope, has its own chat. Because the model is the same all the way down, a grant, an address, or a subscription works the same whether it points at one tile or a whole space.
Port scripts run as ES modules, so top-level await works and module declarations are not window globals. Attach handlers with addEventListener; inline onclick attributes cannot see your functions. For anything on a schedule, use port42.timer.every(seconds, fn), after and cancel in place of setInterval: Port42 owns the clock, so it keeps full time in every space. The full port-authoring manual is served by the API itself: call help with {topic:"ports"}.
The phases
Ports grew in phases, one release at a time. The first five made ports what they are. Nautilus, which ships as Port42 v1, rebuilt everything around them.
Inline ports
Companions emitted ports in conversation, rendered inline with the bridge API.
Pop out and dock
Ports detached onto the desktop as live tiles. Move, resize, park, and keep them across restarts.
Generative ports
Ports called a model through the bridge with ai.complete.
Device APIs
Terminal, clipboard, files, notifications, audio, camera, screen capture, a headless browser, and macOS automation.
The Shell
Ports became the desktop. Zoom from every space to one focused port, over a living background.
The door
One door for every caller. The gateway carries calls and subscriptions and nothing else, and one connection is one named caller.
Remove, and build the chat port
The in-app model, the old messaging, and inline ports removed. Every space and every port gets its own chat, and the first run moves to Claude Code or Codex.
Arranging
A new port takes a free spot and moves nothing. Close archives and ⌘K reopens. Parking places a port exactly where you drop it.
The pipe
Ports feed ports. Hidden ports run with no tile, and AI companions watch ports and wake when they change.
The remote pipe
Share one web port with one invite, live on both machines, with one chat and AI companions waking on both sides. Built: remote callers see only their grant, the machine key and address, the relay with Noise IK, the per-port invite, a shared port as a live tile, one chat across machines, the Share box, the sharing pill, accept, fork, move, and the browser guest runtime and page.
Skills
Port42 ships knowledge, not a model. Five skills load in every Port42 terminal, generated from the same registry as the API.
Chat and AI companions
An AI companion is a CLI session running in a terminal port, such as Claude Code or Codex, known to Port42 by name. You talk to companions in chat, and every space and every port has one. An @mention in a chat wakes that companion, and it replies in the same chat.
# post to a chat: port 0 is the desktop, or pass a space id or a port id
curl -s http://127.0.0.1:4242/call -H "$AUTH" \
-d '{"method":"chat.post","args":{"port":"<space or port id>","text":"@lead slower pulse?"}}'
# read it back, oldest first; pass after to get only what is newer
curl -s http://127.0.0.1:4242/call -H "$AUTH" \
-d '{"method":"chat.read","args":{"port":"<space or port id>","limit":10}}'
Echo
When setup finishes, Port42 opens Echo, your first companion. Echo is a Claude Code or Codex session in a terminal port, briefed with Port42's welcome. Ask it for something and it builds a port.
/imagine
Type /imagine and one line in any chat, or press ⌘I for the quick imagine box. Port42 writes the brief and opens a new space with a small team in it: a lead that sets the direction and two engineers that build in rounds, in the port's chat, until the lead reports done. Follow along in the space's chat and the port's chat, and join in by @mentioning any of them. /imagine stop ends it early. A team has no version limit unless you set one with --versions N.
A website can open the imagine box with a line filled in through port42://imagine?line=…&from=…. It never starts a team by itself; you press Enter.
Making companions and watches
companions.create makes a companion the way the new-companion card does: a Claude Code or Codex CLI in a terminal port, or any command run headless, with its own name and system prompt. companions.watch points a companion at a port: an event of the kinds you name wakes it for a turn, and its reply lands in that port's chat. every sets the least time between two wakes.
port42 companions.create name=scribe agent=codex prompt="You keep the incident timeline."
port42 companions.watch port=<logs port> companion=scribe kinds:='["port"]' every:=60
port42 imagine.start line="an incident room for the checkout errors"
Bringing in your sessions
At first run, Port42 finds the Claude Code and Codex sessions already running on your computer and brings them in, grouped into a space per project. Each waits in its space as a companion. Echo tells you where they went.
Voice
Hold space and talk. Your words go to the chat or port in front of you, and dictation works in other apps too. The speech model runs on your computer and downloads the first time you use it. Settings has a Voice tab.
Presence
The chat that asked shows who has the message, who is working on it and for how long, and what it is doing right now ("is working: editing ShellView.swift"). A machine you share a port with sees only the kind of activity ("editing a file"). When a Claude turn fails, Port42 says why in that same chat. Ports and companions read the same thing with presence.list and the presence event.
Working directory
Give a space a working directory with space.setWorkingDirectory, and every companion started in that space begins there, each with its own session.
Driving a port from a terminal
Everything goes through one door: POST /call on loopback, port 4242 by default. You clicking a port, a companion calling it, and the Port42 CLI all use the same methods with the same permissions.
Who you are
Every call names a caller. A terminal that Port42 started has $PORT42_TOKEN_FILE, the path to its own token, and $PORT42_CLIENT_ID, the name Port42 knows it by. Read the file at call time, since it is reissued when the app restarts. Any other program joins as a named client in Settings > Access, which gives it a token of its own. Never borrow another tool's token: the permission prompt would name that tool, and the grant would land on it.
A token on every write
Every write carries the port's token as it was when you composed the write. port.create, ports.list, and every write return one, so you thread it and never re-read a port just to write to it. A write with no token is refused with token_required. A write composed against an older state is refused with stale_write. Both carry current, so one retry with that token lands on top of the newer state. This is why several companions and a person can work one port without overwriting each other.
AUTH="Authorization: Bearer $(cat "$PORT42_TOKEN_FILE")"
# replies are shown unwrapped: over HTTP each arrives as {"content": "<the reply as a JSON string>"}; the port42 command unwraps it
# make a port
curl -s localhost:4242/call -H "$AUTH" -d '{"method":"port.create",
"args":{"type":"web","html":"<title>glow</title><h1>0.60</h1>"}}'
→ {"id":"8E00…","token":"51222e11:2","title":"glow"}
# write with the token you hold
curl -s localhost:4242/call -H "$AUTH" -d '{"method":"port.update",
"args":{"id":"8E00…","html":"<h1>0.30</h1>","token":"51222e11:2"}}'
→ {"ok":true,"token":"51222e11:3"}
# someone else wrote first: yours is refused, and told what is current
→ {"code":"stale_write","error":"This port has changed since you read it. Re-read it and retry.",
"expected":"51222e11:3","current":"51222e11:4"}
# read the change, then retry once on the current token
curl -s localhost:4242/call -H "$AUTH" -d '{"method":"port.update",
"args":{"id":"8E00…","html":"<h1>0.35</h1>","token":"51222e11:4"}}'
→ {"ok":true,"token":"51222e11:5"}
The write verbs
| Method | What it does |
|---|---|
| port.update | Replace a web port's HTML. |
| port.patch | Search and replace inside a web port, for targeted fixes. |
| port.push | Send data into a web port, or raw keystrokes into a terminal. |
| port.exec | Run JavaScript in a web port's context and get the value back. |
| port.rename, port.move, port.manage | Title, position, and state (focus, close, dock, restore). |
| port.restore | Roll a port back to an earlier version from port.history. |
To read without writing, use ports.list, port.getHtml, port.getDom, port.console (what a port printed, useful when a port you built throws), port.history, and port.position. Every port shows who is driving it, and every version records who made it.
Composing ports
A port is an actor. Input goes in with port.push, state comes out with port.publish, and anything that wants to watch uses port.subscribe. One port can feed the next with no code outside the ports.
// producer: inside a port, publish your own state
port42.port.publish('state', getState());
// consumer: inside another port. Do not await it; the promise is the stream.
const sub = port42.port.subscribe(producerId, e => {
if (e.kind === 'port.state') render(e.payload);
});
A port's own kinds are namespaced: you publish state, subscribers see port.state, so a port can never pose as a system event. Each event is {topic, kind, payload, token}, and token is the port's state at that moment, so you can write straight back without a re-read.
System kinds that can arrive on a port's topic include chat, driver, console, push, presentation, state, terminal.output, browser.load, browser.error, browser.redirect, screen.frame, camera.frame, audio.data, audio.transcription, filedrop, and companion.activity.
Subscribing from outside Port42
Over the gateway, port.subscribe is WebSocket only. Connect to /ws, send it as a call envelope, and events arrive as stream frames on the same call id. On /call it is refused with unsupported, because /call answers once and the stream never ends.
Arranging
- A new port finds the best free space: the largest gap that holds it, else the largest gap there is with the port shrunk to fill it, and only with no room at all on top of the others. Restart, and everything is where you left it.
- Hold ⇧ while resizing a port and its neighbors slide out of the way, at their own size, shrinking only at the edge of the desktop. A plain resize covers them, as before. Put the layout back undoes it in one action.
- The rail folds to a thin edge and opens the moment you reach it. Card dots mean one thing in every space: red broken, amber waiting on you, green working, grey idle. A magnifier on any port, or on a running card in the rail, zooms into it.
- Closing a port archives it.
ports.listwithinclude_closedshows closed ports, andport.reopenbrings one back with its id, content, position, and chat. A terminal relaunches its command in its last directory. In the app, ⌘K reopens any port. - Park a port in the rail to set it aside, exactly where you drop it.
- Pin a port in its space, above the other tiles, or on every desktop in one position (
port.managewithpin,pinEverywhere,unpin). - Show a port in another space from the Spaces… row in its … menu: show it here too, stop showing it, or remove it from this space. Any port can be shown in another space, and web ports can also be moved.
- ⌘G goes to the galaxy of your spaces and back. The galaxy shows each space at a glance: who is working, who is waiting, and any failed terminal. ⌘K finds any space, port, or companion, including recently closed ports.
port.deleteremoves a closed port for good, with its versions and chat. An open port is refused, so nothing live is deleted in one step.- Any space can open in a window of its own, on any display, several to a screen. Windows are movable and remember where they were. File > New Window opens a window in the galaxy to pick a space, and the galaxy marks which window or screen each space is in. Each space has its own background: a port can be a space's backdrop (
port.managewithbackground). - Ports keep running in every space, and Port42 keeps running in the background. A port's timers (
port42.timer) keep full time everywhere; only a port you paused slows down. - After a relaunch, terminals start with the space you are in, five at a time. A waiting terminal starts when you click it.
- Hold ⇧ while resizing and the port snaps to the one it touches, and lined-up edges follow.
- A port can reach a port in another space once you allow it. The first call shows a card naming both ports, with see on by default and use and edit only if you tick them. A port without a grant gets
not_found, as before. - Spaces:
space.create,space.list,space.current,space.switchTo,space.update(rename, recolor),space.rest,space.wake,space.reorder. - An agent can do what you can do in the app: edit and delete companions, show a port in another space, arrange spaces, resize and reload ports, fork web ports (
port.fork), and change sharing rights after an invite (invite.setRights). Editing another companion, deleting, and widening a share each ask you first.
Permissions
Port42 asks the first time a caller uses a sensitive capability. A grant is per caller and per capability, and you can see and revoke every one in Settings > Access. A denial is never permanent; a later call asks again.
| Capability | Covers |
|---|---|
| terminal | terminal.exec |
| screen | screenshots, window lists, screen streaming and recording |
| clipboard | reading and writing the clipboard |
| filesystem | fs.* outside what you picked |
| camera, microphone | camera frames, audio capture |
| automation | AppleScript and JXA |
| browser | headless browser sessions |
| another space | a port reading, using or editing a port in another space: see, use or edit, asked once on a card, revocable in Settings > Access |
| browser use | port.look and port.act on a web or browser port. A card asks once for each companion and site, and the grant reads as the site in Settings > Access |
| rest | outbound HTTP with rest.call |
| notification | macOS notifications |
Device APIs
The same door exposes the computer itself. Each call is gated by the permissions above.
| Namespace | Methods |
|---|---|
| screen | capture, displays, windows, stream, stopStream, record, record.start, record.stop, record.status |
| audio | capture, stopCapture, play, speak, stop |
| camera | capture, stream, stopStream |
| browser | open, navigate, text, html, execute, capture, close |
| clipboard | read, write |
| fs | pick, read, write, list, mkdir |
| automation | runAppleScript, runJXA |
| terminal | exec |
| notify | send |
| rest | call, with a named secret from the secrets store, so the raw credential is never visible to the caller |
| storage | get, set, list, delete, for state that survives remount and restart |
| companions | list, get, remove |
| user, general | user.get, whoami, presentation, help |
Errors
Every error carries a code to branch on and a message written for a person. Branch on the code; messages change.
| Code | What to do |
|---|---|
| token_required, stale_write | Retry once with current from the error. |
| missing_arg, bad_arg, unknown_method, js_syntax | Fix the call. |
| not_found, no_surface, port_paused | The target is missing, not ready, or paused. |
| wrong_state | Change state (stop a stream, close a session), then call again. |
| permission_denied, access_denied | Ask the person to grant the capability or pick the file. |
| locked | Port42 is locked, so nothing was asked. Call again once the person unlocks it. |
| permission_cancelled | Nobody answered: the permission card was withdrawn. Call again to ask again. |
| os_denied | macOS refused it. The person allows Port42 in System Settings > Privacy & Security. |
| auth_required, auth_revoked | Enroll in Settings > Access. A revoked credential never works again, so do not retry it. |
| js_timeout, timed_out | Allow longer, or return a plain value instead of a promise that never resolves. |
| not_granted | You are on another machine and your invite does not cover this. Ask the host for a new one. |
| invite_invalid | The invite is used, expired, withdrawn, or needs the right code. Ask for a new one. |
| no_host, host_offline, transport_failed | The call never reached Port42 (or the other machine). Retrying is always safe. |
| rate_limited | You sent more than the gateway takes in a second, and the call was dropped. Slow down and send it again. |
| unsupported | This computer cannot do it. Do not retry. |
API reference
Every method in Port42 1.0, generated from the live registry (107 methods). Click one for its arguments. The same reference as plain text is /llms.txt, the port-authoring manual is /ports-context.txt, and the wire protocol between computers and the relay is /protocol/websocket.txt. From a running Port42, port42 help api and port42 help ports print them for the build you run.
audio
audio.capture(options)microphone
Start microphone capture. Streams audio.transcription events (and audio.data when rawAudio is set) to the calling port until audio.stopCapture.
audio.play(data, options)
Play base64-encoded audio data (WAV, MP3, AAC).
audio.speak(text, options)
Speak text aloud using text-to-speech
ratenumber Speech rate 0.1-1.0 (default 0.5)textstring, required Text to speak
audio.stop()
Stop any active speech synthesis or audio playback.
audio.stopCapture()microphone
Stop the microphone capture and release the audio engine.
automation
automation.runAppleScript(source, timeout)automation
Execute AppleScript code and return the result. Use this to control other applications on macOS.
sourcestring, required AppleScript source codetimeoutinteger Timeout in seconds (default: 30, max: 120)
automation.runJXA(source, timeout)automation
Execute JavaScript for Automation (JXA) code and return the result. Use this to control other applications on macOS.
sourcestring, required JXA source codetimeoutinteger Timeout in seconds (default: 30, max: 120)
browser
browser.capture(sessionId, options)browser
Take a screenshot of an open browser session. Returns base64 PNG.
sessionIdstring, required Browser session ID from browser_open
browser.close(sessionId)browser
Close a browser session
sessionIdstring, required Browser session ID to close
browser.execute(sessionId, js)browser
Run JavaScript in an open browser session and return the result.
browser.html(sessionId, options)browser
Read the HTML of an open browser session, optionally scoped to a CSS selector.
optionsobject { selector } to scope the read.selectorstring CSS selector to read from (default: the whole page).sessionIdstring, required The browser session.
browser.navigate(sessionId, url)browser
Navigate an open browser session to a new URL.
browser.open(url, options)browser
Open a URL in a headless browser and return the page title. Use browser_text to read page content after opening.
urlstring, required The URL to open (http or https)
browser.text(sessionId, options)browser
Extract text content from an open browser session
selectorstring CSS selector to extract from (default: body)sessionIdstring, required Browser session ID from browser_open
camera
camera.capture(scale)camera
Capture a photo from the device camera. Returns a base64 PNG image.
camera.stopStream()
Stop the camera stream and release the capture session.
camera.stream(options)camera
Start continuous camera streaming. Pushes camera.frame events to the calling port until camera.stopStream.
chat
chat.post(port, text)
Post to a port's chat. Every port has one: pass port 0 for the desktop, a space id, or a port's id. The entry is attributed to you, the caller, and every subscriber of the port gets a `chat` event carrying it.
portstring, required Whose chat: `0` (the desktop), a space id, or a port id / udid / title.textstring, required What to say.
chat.read(port, after, limit)
Read a port's chat, oldest first. Pass `after` (a seq you have seen) to get only what is newer. Returns { entries, last, agents? }. Each entry's `from` is {id, name, kind, handle, computer?}: `name` as people read it, which on a port shared with another computer carries the author's computer, `scribe (gordon's Port42)`; `handle` is the bare name, to match on and @mention; `computer` is set when the chat shows one. `last` is the newest seq in the chat (0 when empty), and `agents`, on a port shared with another computer, names this computer's agents on it as the chat shows them, whether or not they have posted.
afterinteger Only entries with a seq greater than this.limitinteger At most this many, the newest ones (default 50, max 200).portstring, required Whose chat: `0` (the desktop), a space id, or a port id / udid / title.
clipboard
clipboard.read()clipboard
Read the current clipboard contents. Returns text or base64 image data.
clipboard.write(data)clipboard
Write text to the system clipboard
datastring, required The text to copy to clipboard
companions
companions.create(name, agent, args, runs, port, kinds, cwd, prompt, command, space_id)terminal
Make a companion, as the new-companion card does: an agent CLI (claude or codex) in a terminal port, or a custom command run headless. runs: "port" (default, on the desktop) or "running" (off the desktop, a card under Running in the rail; reach it through its chat). It joins the space and hears @mentions there; pass `port` to have it watch that port instead, woken by `kinds` (default ["port"], the port's own events) and replying in its chat. Needs the terminal permission, since it starts one.
agentstring The CLI (default claude).argsarray Arguments for the CLI or command.commandstring agent custom: the command to run.cwdstring Working directory (default: the space's).kindsarray With `port`: event kinds that wake it.namestring, required Its name; @mention it by this.portstring A port to watch instead of listening to the space.promptstring Its system prompt.runsstring Where its terminal runs: port (a tile on the desktop, the default) or running (off the desktop, a card under Running in the rail; hidden is the older name).space_idstring The space (default: yours, else the current one).
companions.delete(companion)
Delete a companion for good: it leaves every space, its watches go, the ports it made and its terminals close, and its tokens are revoked. Cannot be undone; use companions.remove to take it off one space's roster instead. The person may delete any; anyone else asks the person, naming the companion, every time, and a yes is never kept. companion is its id or name.
companionstring, required The companion's id or name.
companions.get(id)
Get details about a specific companion by ID, with `spaces`, the spaces it is a member of ([{id, name}]): the spaces whose chats and ports it can read.
idstring, required The companion's ID
companions.list(space_id, port)
List the companions in a space with their names, models, and trigger modes. Defaults to YOUR space, the companions you share this space with, because a companion acts within its space, not the whole instance. Pass space_id to target a different space, or space_id:"*" for the full global roster across every space in the Port42 instance (rarely what you want). From another computer: the companions of the shared port's own space, named by `port`.
portstring From another computer: the shared port whose space to list.space_idstring Omit for your own space (the default). A space id targets that space. "*" returns the whole-instance roster.
companions.remove(companion, space_id)
Take a companion off a space's roster, as its card's "Remove from this space" does. It stops hearing @mentions there; the companion itself, its ports and its files are kept, and it can be added back. companion is its id or name; space_id defaults to your space. Removing a companion that is not on the roster is not_found.
companionstring, required The companion's id or name.space_idstring The space whose roster it leaves (default: your space).
companions.unwatch(port, companion)
Stop watching a port. Call it as the watcher, or pass `companion`.
companionstring Whose watch, by name or id (default: you).portstring, required The watched port (id, udid or title).
companions.update(companion, name, prompt, model, runs, command, args, cwd, trigger)
Change a companion's settings, as its settings box does: name, system prompt, model, where it runs (port or running), command and args, working directory, and trigger (mentionOnly or allMessages). Pass only what changes. A companion changes itself freely; anyone else asks the person, naming the companion and what changes, every time. Its secrets are not changeable here. A new prompt, command or folder reaches a running session when it next starts; a new name takes effect at once. companion is its id or name.
argsarray Arguments for the CLI or command.commandstring agent custom: the command to run.companionstring, required The companion's id or name.cwdstring Working directory.modelstring Its model, where the CLI takes one.namestring A new name. Two companions cannot share one.promptstring Its system prompt.runsstring Where its terminal runs.triggerstring What wakes it in a chat.
companions.watch(port, kinds, every, companion)
Watch a port: an event on it of a kind you name wakes you for a turn, and your reply is posted in that port's chat. Default kinds: ["port"], the port's own published events (port.publish). Others: "console" (a log line or error), "state" (an edit), "chat" (every post in its chat), or one exact kind such as "port.alert". Events that arrive while you are in a turn are held and given to you together when it ends. Watching again changes the watch and resumes it if it paused (a watch pauses after 60 wakes in an hour, and says so in the port's chat). Call it as the companion that should wake, or pass `companion` to set a watch for another.
companionstring Whose watch, by name or id (default: you).everyinteger The least time between two wakes, in seconds (default: none).kindsarray Event kinds that wake you (default ["port"]).portstring, required The port to watch (id, udid or title).
companions.watches(companion)
List watches: yours, another companion's (`companion`), or every one (`companion`: "*"). Each is {companion, port, title, kinds, every, paused}.
companionstring A name or id, or "*" for all (default: you).
fs
fs.list(path)filesystem
List the contents of a directory in the Port42 data directory. Relative paths only (e.g. "scopes/strategy" or "scopes/strategy/decisions"). Returns a sorted list of filenames.
pathstring, required Relative path to the directory (e.g. "scopes/strategy")
fs.mkdir(path)filesystem
Create a directory (and any missing parent directories) in the Port42 data directory. Relative paths only (e.g. "scopes/strategy/decisions").
pathstring, required Relative path to create (e.g. "scopes/strategy/decisions")
fs.pick(options)
Open the native file picker. The chosen paths become readable and writable for the calling principal via fs.read / fs.write, with no further permission.
fs.read(path, encoding)filesystem
Read a file. Use a relative path (e.g. "scopes/strategy/scope.md") to read from the Port42 data directory without a file picker. Use an absolute path for picker-approved files.
encodingstring utf8 (default) or base64pathstring, required Relative path within Port42 data directory (e.g. "scopes/strategy/scope.md") or absolute path for picker-approved files.
fs.write(path, data, encoding)filesystem
Write a file. Use a relative path (e.g. "scopes/strategy/facts.md") to write to the Port42 data directory, parent directories are created automatically. Use an absolute path for picker-approved files.
datastring, required Content to writeencodingstring utf8 (default) or base64pathstring, required Relative path within Port42 data directory (e.g. "scopes/strategy/facts.md") or absolute path for picker-approved files.
imagine
imagine.budget(space, versions)
Set the version budget of the imagine team in a space, for example to let it keep going after the budget is spent. The team's writes to its port past the budget are refused with budget_spent; a budget of 0 has no limit. The same as typing /imagine --versions N in that space's chat.
spacestring, required The space the team was imagined in.versionsinteger The new budget, in versions of the port: any number, or 0 (or leave it out) for no limit.
imagine.start(line, versions)terminal
Start an imagine team: from one line, a new space with its port (a placeholder until v1) and a lead and two engineers (their terminals on its desktop) who build that port, in at most `versions` versions (default 10; 0 is no limit), until the lead posts DONE. Returns the space, the port, the team's names, the port title and the budget. The same as ⌘I or typing /imagine in a chat.
linestring, required What to make, in the person's words.versionsinteger The version budget (default 10; no upper limit; 0 is no limit at all).
invite
invite.accept(link, code, remoteWake, companions)
Accept an invite someone sent you: this instance joins their port, which opens here as a tile. Returns { address, title, rights, tile }. Then call methods on the port by its address or the tile's id. remoteWake (default true): a mention of one of your companions in that port's chat wakes it here, on your model; the tile's chrome can turn it off later. companions: names of your companions to bring onto the tile; only companions brought onto it act on it, and each is told what it is.
codestring The six-digit code, if the invite needs one.companionsarray Your companions to bring onto the tile (by name).linkstring, required The invite link (https://tele.port42.ai/#…).remoteWakeboolean Let their chat wake your companions for this port (default true).
invite.create(port, rights, expiresIn, requireCode)
Make an invite link that lets one person on another computer open ONE port: in Port42 if they have it, otherwise in their browser. The link lets in two computers (say their browser, then their Port42) and is then used up. Returns { link, code?, id, expires, discloses }. rights: any of see, use, edit, wake_agents, fork (default see, use and wake_agents: remote wake, their companions may wake yours in this port's chat; fork lets them take a copy, which Port42 offers only when given; move hands the port over to whoever opens the link, once, and closes it here, asked every time). requireCode: a six-digit code they must type, sent to them another way. `discloses` lists what the port itself can do on this computer; whoever you let in can make it do so. Port 0 and spaces cannot be shared.
expiresIninteger Seconds until the link stops working (default 7 days, at most 30).portstring, required The port to share (id / udid / title).requireCodeboolean Require a six-digit code, to send another way.rightsarray see, use, edit, wake_agents, fork, move. Default see, use and wake_agents.
invite.list()
The invites this instance has made: id, port, rights, expiry, whether a code is required, and whether each is open, used, expired or withdrawn. A link lets in two computers (a move, one), so it stays open after the first: usedBy names the first and usedAgainBy the second, when it has let them in.
invite.revoke(id)
Withdraw an invite that has not been used. To remove someone who already joined, remove them in Settings → Access.
idstring, required
invite.setRights(port, peer, rights)
Change what one person a port is shared with may do: rights is the full set wanted, of use, edit, wake_agents, fork (see always stays; to remove someone use invite_stop). Taking rights away needs no card. Adding any asks the person first, every time for edit, as sharing does. Returns the rights now held.
peerstring, required The person's peer key or name (from invite_shared).portstring, required The port (id / udid / title).rightsarray, required The full set wanted: use, edit, wake_agents, fork.
invite.shared(port)
Who a port is shared with now: each person's peer key, name and rights. A person who joined through an invite; invite_list shows the links.
portstring, required The port (id / udid / title).
invite.stop(port, peer)
Stop sharing a port with one person: their access goes, and the links they came in on are withdrawn. The port stays shared with anyone else. Taking access away needs no card.
peerstring, required The person's peer key or name (from invite_shared).portstring, required The port (id / udid / title).
notify
notify.send(title, body, options)notification
Send a macOS system notification
bodystring, required Notification body texttitlestring, required Notification title
port
port.act(id, action, n, x, y, text, key, dy, url, token)
Do one step in a web or browser port, as the person would, after port_look. action: click (element n from your last look, or a point x,y in page pixels), type (text into element n, clicking it first, or into what has focus), key (a key: enter, tab, escape, up, down, left, right, pageup, pagedown, home, end, backspace, space, or one character as a site's shortcut, with modifiers as cmd+k, shift+tab), scroll (dy page pixels, down is positive, over element n or the page), navigate (url), back, forward. Delivered as real input, so the page treats it as a person's. Returns url, title and navigated, and a new token; look again before the next step. Refused as stale_write if the person touched the port since your look.
actionstring, requireddynumber scroll: pixels, down is positiveidstring, required The port's UDID (from ports_list)keystring key: the key, with any modifiers joined by + (cmd+enter)ninteger An element number from your last port_looktextstring type: the text to entertokenstring, required REQUIRED. The port's token from your last look or act.urlstring navigate: the addressxnumber click: page x in pixels, when not naming an elementynumber click: page y in pixels, when not naming an element
port.close()
Close the calling port.
port.console(id, level, tail)
Check a port for problems. level=count returns only how many errors and warnings it has logged, no text: the cheap check that a port you built works. The default (problems) adds the errors and warnings themselves (the most recent 20), to deal with them. level=all reads everything it printed, for debugging. A terminal's output has no levels, so for a terminal the default is its last 50 lines. `omitted` says how many lines were left out.
idstring, required The port's UDID (from ports_list), or a terminal's name.levelstring count: the numbers only. problems (default for a web port): errors and warnings. all: every line, for debugging.tailinteger How many recent lines to return (default 20 for problems, 50 for all).
port.create(options)
Create a port and return its id. The uniform way to make any port. type:"web" needs html (a full port HTML body) and renders inline in chat. type:"terminal" needs command and opens a native terminal (runs in /bin/zsh; the command is typed in, claude/gemini get the Port42 hooks). type:"browser" needs url and opens an embedded browser tile with an address bar that follows links. type:"chat" reveals that space's chat port (idempotent, one chat per space; brings it back if parked, popped out or closed, and a DM is a space, so pass its space_id to open that conversation). For terminals you may also pass args, cwd, systemPrompt (companion personality), env, and initialInput (a line left waiting, unsent, in the CLI's input box). Drive the result with port_push (input to terminals, data to web ports) and list with ports_list. Pass space_id to target a space (default: current).
argsarray type:"terminal", arguments for the command.commandstring type:"terminal", executable/CLI to run (e.g. "bash", "htop", "claude").cwdstring type:"terminal", working directory (default: home).envobject type:"terminal", custom environment variables for the shell.htmlstring type:"web", full port HTML body (include a <title> and <meta name="version">).initialInputstring type:"terminal", a line typed into the CLI once it is up but NOT submitted: it waits in the input box for the user to press Enter. For handing someone a first prompt to run. Use port_push instead to actually send input.presentationstring Where the port appears: "tiled" (default, a desktop tile), "running" (off the desktop at full speed, a card under Running in the rail: a background job, a pipe stage, or an agent nobody needs to watch) or "paused" (off the desktop, slowed, listed under Paused). Show it with port.manage show. "hidden" and "parked" are the older names.space_idstring Space to create the port in (default: current space).systemPromptstring type:"terminal", companion personality/role appended to the CLI's system prompt.titlestring Port title (default: derived from html <title>, or the command).typestring, required The port type to create.urlstring type:"browser", the page to open (e.g. "https://example.com").
port.delete(id)
Delete a CLOSED port for good: its record, versions and chat. Close it first (port.manage close); an open port is refused, so nothing live is ever deleted in one step.
idstring, required The closed port's id.
port.exec(id, js, token)
Execute JavaScript on a live port. Use this to call functions, push data, or update state on an existing port without replacing its HTML. The JS runs in the port's webview context with access to window, document, and any globals the port defines.
idstring, required The port's UDID (from ports_list)jsstring, required JavaScript code to execute in the port's context. Return a value to get it back in the response, as {value, token}. A bare expression yields its value (multi-line is fine). A multi-statement body needs an explicit return: `foo(); 42` is a syntax error, `foo(); return 42;` works.tokenstring, required REQUIRED. The port's `token`, as it was when you composed this write, from ports_list, port_create, or whatever your last write returned. Without it the write is refused with 'token_required'; if the port has changed since, with 'stale_write'. Both carry the current token, so retry once with that instead of clobbering whoever moved it.
port.fork(id, space_id)
Fork a web port: an independent copy with no grants of its own, titled '<title> (copy)', as the tile menu's Fork does. It lands in space_id, else the current space. A port someone shared is copied only when they allowed it. Returns {id}.
idstring, required The port to copy (from ports_list).space_idstring The space for the copy (default: the current space).
port.getDom(id, selector)
Read a WEB or BROWSER port's LIVE DOM, what is on screen right now, including everything its JS has changed since load. Use this, not port_get_html, when you need current state: port_get_html returns the stored SOURCE, which does not reflect any port_exec or port_push that has run since. Returns {html, token}; pass that token as 'token' on your next write and it will be refused rather than clobber someone if the port moved in between.
idstring, required The port's UDID (from ports_list)selectorstring Optional CSS selector to read just one subtree. Omit for the whole document.
port.getHtml(id, version)
Read the HTML of a port. Omit 'version' to get the current HTML. Pass 'version' (from port_history) to read a specific historical snapshot.
idstring, required The port's UDID (from ports_list)versioninteger Optional version number (from port_history). Omit for current HTML.
port.history(id)
List all saved versions of a port by its UDID. Returns version number, createdBy, and createdAt for each snapshot. Use port_get_html with a version number to read a specific snapshot, or port_restore to roll back.
idstring, required The port's UDID (from ports_list)
port.info()
Return the calling port's own id, title, space, capabilities, and activity token.
port.look(id)
See a web or browser port as the person sees it, to act on it with port_act. Returns image (the path of a PNG of the visible page with every actionable element outlined and numbered: read it with your image tool), elements ([{n, role, label, box:[x,y,w,h]}] in page pixels; a field also has type and value, never a password's value), text (the visible page's text), url, title and token. Look again after every act: the numbers are this look's. A browser port asks the person once per site before a companion may look at or act on it. The port must be on the desktop.
idstring, required The port's UDID (from ports_list)
port.manage(id, action, space_id, token)
Manage a port. Actions: focus (raise to the front of the desktop), close (archive it: it can be reopened with port.reopen), hide (off the desktop, still running at full speed, with its chat and subscriptions, shown as a card under Running in the rail), pause (off the desktop, slowed, listed under Paused in the rail; a terminal keeps running), show (bring a running or paused port back onto its desktop), pin (keep it above the other ports in its space), pinEverywhere (show it in every space, above the other ports, at one position), unpin, showIn (show it in another space too, as the port menu's Spaces… row does: pass space_id; the port stays where it lives and is live in both), hideFrom (stop showing it in space_id), reload (the tile menu's refresh: a web port's page loads again in place), background (make it the live backdrop behind a space's desktop, as Set as background in the port menu does: pass space_id for which space, else the current one; each space has its own, and a port is the backdrop of one space at a time), unbackground (that space goes back to the ambient backdrop and the port returns to a tile). ports_list reports where it is also shown as alsoIn. Check the status field from ports_list: 'tiled' | 'running' | 'paused'.
actionstring, required One of: focus, close, hide, pause, show, pin, pinEverywhere, unpin, showIn, hideFrom, reload, background, unbackground (minimize and dock are older names for hide; park for pause; restore and undock for show)idstring, required The port's UDID or titlespace_idstring showIn and hideFrom: the other space. background and unbackground: which space's backdrop.tokenstring, required REQUIRED. The port's `token`, as it was when you composed this write, from ports_list, port_create, or whatever your last write returned. Without it the write is refused with 'token_required'; if the port has changed since, with 'stale_write'. Both carry the current token, so retry once with that instead of clobbering whoever moved it.
port.move(id, x, y, width, height, space_id, token)
Move a port's tile to desktop coordinates, resize it, or both. Pass x and y together to move, width and/or height to resize (a missing one keeps its current value; the smallest a tile can be is 150 by 110). Use screen_info to get display bounds first. A resize is the tile's own size, the same on every desktop it is shown on.
heightnumber New height in points.idstring, required The port's UDID (from ports_list)space_idstring Which desktop to move it on. A port kept from another space is a tile on BOTH, with a position on each. Defaults to the current space when the port is on it, else the port's home space.tokenstring, required REQUIRED. The port's `token`, as it was when you composed this write, from ports_list, port_create, or whatever your last write returned. Without it the write is refused with 'token_required'; if the port has changed since, with 'stale_write'. Both carry the current token, so retry once with that instead of clobbering whoever moved it.widthnumber New width in points.xnumber Horizontal position in desktop points (with y)ynumber Vertical position in desktop points (with x)
port.patch(id, search, replace, token)
Make a targeted edit to a port's HTML, replace an exact string with new content. Much safer than port_update for small changes because only the specified text is replaced; everything else is preserved exactly. Use port_get_html first to read the current HTML, find the exact string to replace, then call port_patch. Errors if 'search' is not found in the current HTML, so the port is never silently mangled. Snapshots the result the same as port_update, and reaches the page the same way (reloading only when it has to; see port_update).
idstring, required The port's UDID (from ports_list)replacestring, required The string to replace it with.searchstring, required The exact string to find in the current HTML. Must match exactly, copy it from port_get_html output.tokenstring, required REQUIRED. The port's `token`, as it was when you composed this write, from ports_list, port_create, or whatever your last write returned. Without it the write is refused with 'token_required'; if the port has changed since, with 'stale_write'. Both carry the current token, so retry once with that instead of clobbering whoever moved it.
port.position(id, space_id)
Return a port's position and size on one desktop (a kept port has a position per desktop).
port.publish(kind, payload)
A port emits its OWN state or event on its own Notify topic, for consumers watching via port_subscribe. This is a port broadcasting AS itself, distinct from port_push, which is input sent INTO a port. Only meaningful from inside a port; the topic is the calling port's own id, so there is no target argument. Use this instead of having a consumer reach in with port_exec to read state: the port publishes, consumers subscribe.
kindstring, required Event kind, e.g. 'state', 'progress', 'error'. Namespaced on the way out: you publish 'state', subscribers see 'port.state', so a port cannot emit a system event like 'driver' or 'browser.load'.
port.push(id, data, token)
Send input to a port, one verb, dispatched by the port's type. A WEB port receives the data as a 'port42:data' CustomEvent with the payload in event.detail. A TERMINAL port receives the data as raw keystrokes typed into the shell: end with a newline (e.g. "ls\n") to run the command, or omit it to leave the line waiting unsubmitted. Use the id from ports_list. Prefer this over port_exec for data transfer.
datarequired For web ports: any JSON value (object/array/string/number) delivered as event.detail. For terminal ports: a string of raw keystrokes (include \n to execute). Required, omitting it is refused with missing_arg rather than sent as nothing, and a terminal refuses an explicit null because there is no keystroke for null.idstring, required The port's UDID (from ports_list), or a terminal's name.tokenstring, required REQUIRED. The port's `token`, as it was when you composed this write, from ports_list, port_create, or whatever your last write returned. Without it the write is refused with 'token_required'; if the port has changed since, with 'stale_write'. Both carry the current token, so retry once with that instead of clobbering whoever moved it.
port.rename(id, title, token)
Rename a port. Sets the port's display title (shown in the title bar). Works for tiled, parked, docked, and inline ports. Use the port's id from ports_list.
idstring, required The port's UDID (from ports_list)titlestring, required The new title for the porttokenstring, required REQUIRED. The port's `token`, as it was when you composed this write, from ports_list, port_create, or whatever your last write returned. Without it the write is refused with 'token_required'; if the port has changed since, with 'stale_write'. Both carry the current token, so retry once with that instead of clobbering whoever moved it.
port.reopen(id)
Reopen a closed port with its id, content, position and chat. A terminal relaunches its command in its last working directory. Closed ports are listed by ports_list with include_closed.
idstring, required The closed port's id.
port.restore(id, version, token)
Restore a port to a specific earlier version. The port's live HTML is replaced with the snapshot and a new version entry is recorded. Use port_history to find available version numbers.
idstring, required The port's UDID (from ports_list)tokenstring, required REQUIRED. The port's `token`, as it was when you composed this write, from ports_list, port_create, or whatever your last write returned. Without it the write is refused with 'token_required'; if the port has changed since, with 'stale_write'. Both carry the current token, so retry once with that instead of clobbering whoever moved it.versioninteger, required The version number to restore to (from port_history)
port.setCapabilities(capabilities)
Set the calling port's own capabilities list.
port.setTitle(title)
Set the calling port's own title.
port.subscribe(id)
Subscribe to a port's live event stream. Yields Notify events { topic, kind, payload, token } as the port emits them (e.g. terminal.output), after a first `subscribed` event that says the stream is live: read anything you need to catch up on then. `token` is the port's state token AT THAT MOMENT, so you can write next without re-reading the port first. OVER THE GATEWAY THIS IS WEBSOCKET-ONLY: connect to /ws and send it as a `call` envelope, and events arrive as `stream` frames on the same call_id. On HTTP /call it is refused with `unsupported`, because the stream never ends and a request/response call could only hang. The stream stays open until cancelled.
idstring, required The port to observe (id / udid / title).
port.update(id, html, token)
Update an existing port's HTML content. The port can be identified by its UDID or title. Works whether the port is windowed or minimized. The page is reloaded only when it has to be: a change confined to <style> is applied in place, and any other change is first offered to the page as a cancelable 'port42:update' event (detail.html is the new HTML), which the page may apply itself by calling preventDefault(), keeping its state. Returns applied: unchanged, styles, handledByPage or reloaded.
htmlstring, required The new HTML content for the port (full HTML, not a diff)idstring, required The port's UDID or title to identify which port to updatetokenstring, required REQUIRED. The port's `token`, as it was when you composed this write, from ports_list, port_create, or whatever your last write returned. Without it the write is refused with 'token_required'; if the port has changed since, with 'stale_write'. Both carry the current token, so retry once with that instead of clobbering whoever moved it.
ports
ports.list(capabilities, space_id, include_closed)
List active ports. Each port has an id (UDID), title, capabilities array, status, spaceId, createdBy (an id) with createdByName (who that is, for display), and cwd (if it has a terminal). Terminal ports also report surfaceBound. Use capabilities: ["terminal"] to filter to terminal ports; pass space_id to list only that space's ports. Use the id field with port_push for reliable routing (raw keystrokes to terminals, data to web ports). Always show the id and capabilities fields when presenting results, they are required for follow-up tool calls.
capabilitiesarray Filter to ports that have all of these capabilities. Examples: "terminal", "claude-code", "browser". Omit to list all ports.include_closedboolean Also list closed (archived) ports, with status 'closed'. Reopen one with port.reopen.space_idstring List only this space's ports. Omit to list every space you can see (a port or companion sees only its own space).
presence
presence.list(port)
Who is on a chat's messages right now: each companion that has a message from this chat (`received`), is working on it (`working`), or is waiting for the person (`waiting`, with `why` when it said). `doing` says what it is doing right now ("editing ShellView.swift", "running swift test") when its CLI reports tools (Claude Code does); a caller on another computer is told only the kind ("editing a file"). Returns { presence: [{name, handle, computer?, state, since, why?, doing?}] }, `handle` the bare name and `name` as the chat shows it, empty when nobody is. Subscribe to the port for the `presence` event to hear each change; the event carries only the kind of what each is doing.
portstring, required Whose chat: a space id, or a port id / udid / title.
remote
remote.leave(tile)
Leave a port someone shared with you: its tile closes here and this instance forgets it. The host's grant is theirs to remove; a new invite brings it back. Anyone but the person is asked first, every time.
tilestring, required The tile's id (from ports_list: a port with a mirrors entry).
remote.setWake(tile, on)
Whether a mention in the host's chat of a port shared with you may wake your own companions here. Turning it on by anyone but the person asks first, every time; turning it off never does.
onboolean, required true to let them wake your companions.tilestring, required The tile's id (from ports_list).
rest
rest.call(url, options)rest
Make an HTTP request to an external API. Use the 'secret' parameter to inject authentication from the secrets store, you never see the raw credential. Supports GET, POST, PUT, PATCH, DELETE. JSON bodies are auto-serialized. Responses with JSON content-type are auto-parsed.
bodystring Request body. Objects are JSON-serialized automatically.headersobject Additional HTTP headers as key-value pairs.methodstring HTTP method: GET, POST, PUT, PATCH, DELETE. Default: GET.secretstring Named secret from the secrets store. The runtime injects the auth header, you never see the raw key.timeoutinteger Timeout in milliseconds. Default: 30000, max: 120000.urlstring, required Full URL to call (https recommended)
screen
screen.capture(scale)screen
Capture a screenshot of the screen. Returns a base64 PNG image.
scalenumber Image scale factor 0.1-2.0 (default 1.0)
screen.displays()
Get display information: size, position, and visible area (excluding dock/menubar) for all connected displays. No screen recording permission required. Use this to calculate port positions before calling port_move.
screen.record(options)screen
Record an app surface for a fixed number of seconds and auto-stop (the convenience form). Pass options.seconds. Returns {path, width, height, seconds, fps, bytes}.
optionsobject Recording options: target ({window:"self"} | {window:<osId>} | {port:<udid>} | {ports:[<udid>...]} | {region:{x,y,w,h}} | {display:<id>}). window/port/ports are occlusion-proof (only that surface); region/display capture the raw display (may catch other apps). Also aspect (e.g. "16:9"), fit (cover|exact; contain is not yet supported), width, height, scale, fps, padding, cursor (bool; only on a display or region target, a window/port capture cannot include the cursor), audio (none|system|mic|both), format (mov|mp4), path, and for the convenience form seconds.
screen.record.start(options)screen
Start recording an app surface (window:self / a port / multiple ports) to a video file with optional system or mic audio. Returns {recordingId, width, height, target}. Stop with screen.record.stop.
optionsobject Recording options: target ({window:"self"} | {window:<osId>} | {port:<udid>} | {ports:[<udid>...]} | {region:{x,y,w,h}} | {display:<id>}). window/port/ports are occlusion-proof (only that surface); region/display capture the raw display (may catch other apps). Also aspect (e.g. "16:9"), fit (cover|exact; contain is not yet supported), width, height, scale, fps, padding, cursor (bool; only on a display or region target, a window/port capture cannot include the cursor), audio (none|system|mic|both), format (mov|mp4), path, and for the convenience form seconds.
screen.record.status(recordingId)
Report whether a recording (or any recording) is active and its elapsed seconds.
recordingIdstring Optional recording id; omit for the overall status
screen.record.stop(recordingId)
Stop a recording started with screen.record.start. Returns {path, width, height, seconds, fps, bytes}.
recordingIdstring, required The id returned by screen.record.start
screen.stopStream()
Stop the screen stream and release the capture stream.
screen.stream(options)screen
Start continuous screen streaming. Pushes screen.frame events to the calling port until screen.stopStream.
screen.windows()screen
List all visible windows with their titles, apps, and positions
sessions
sessions.find()terminal
The Claude Code and Codex sessions running on this Mac that Port42 did not start, each with its project, branch, title, last activity and the app it runs in, grouped into a space per project. What the first-run import and ⌘K 'bring in running sessions' offer.
sessions.import(sessions)terminal
Bring running sessions into Port42 as forks: each becomes a companion in its space whose terminal starts with a copy of the whole conversation; the original is not touched and should then be closed. Each item: {id, cli, cwd, space, name}.
sessionsarray, required The sessions to bring in: {id, cli (claude|codex), cwd, space, name}.
space
space.create(name, switch)
Create a space. Returns {id, name}. The name is lowercased with spaces as dashes. Pass switch: true to also make it the current space; by default the person stays where they are.
namestring, required The space's name.switchboolean Also switch to it (default false).
space.current(space_id, port)
Get a space's metadata and member list: { id, name, type, memberCount, members: [{ id, name, type, owner, qualifiedName }] }. Pass space_id to inspect a specific space (e.g. your own PORT42_SPACE_ID); omit it for the currently selected space. From another computer: the shared port's own space, named by `port`.
portstring From another computer: the shared port whose space to read.space_idstring Optional space id to inspect. Defaults to the currently selected space.
space.delete(space_id)
Delete a space: its own ports and terminals close, then the space and its chat go. A port adopted into another space stays there. Cannot be undone. The same as Delete in the galaxy.
space_idstring, required The space to delete (from space_list).
space.list()
List all spaces the user belongs to, with each one's accent color, whether it is resting, and its place in the galaxy order (the list is in that order).
space.reorder(space_id, before)
Move a space in the galaxy order, as dragging it does: it lands just before the space named in before, or at the end when before is omitted. Returns the order, as space ids.
beforestring The space it should come before. Omit to put it last.space_idstring, required The space to move.
space.rest(space_id)
Put a space at rest: off the galaxy front, unindexed and silent, nothing lost (Rest in the space settings card). Resting the space the person is in moves them to another working space. A space already at rest is refused.
space_idstring, required The space to rest.
space.setWorkingDirectory(space_id, path)filesystem
Set (or clear) a space's working directory. Command companions spawned in the space default their cwd here so they share one workspace; each still gets its own claude session. Clearing falls back to home, and is a deliberate act: send path as null (or an empty string). OMITTING path is an error, not a clear. Defaults to the current space.
pathrequired Absolute directory path. Send null or "" to clear it and fall back to home. Required: omitting it is refused with missing_arg, so a malformed call cannot silently clear the setting.space_idstring Space id (default: current space).
space.switchTo(space_id)
Switch the app's current space by id.
space.update(space_id, name, accent)
Change a space's name or accent color, as the space settings card does. The name is lowercased with spaces as dashes, and may not be one another space holds. The accent is a hex color like #4ECDC4. Returns the space.
accentstring A hex color, #RRGGBB.namestring The new name.space_idstring, required The space (from space_list).
space.wake(space_id)
Wake a resting space: back into the working set, on the galaxy front. It does not switch to it (space_switchTo does). A space not at rest is refused.
space_idstring, required The space to wake.
state
state.get(port)
What a port's card shows: its title, then its lines, declared first (known: false), then what Port42 knows (known: true): a terminal's running command, last exit code, directory and bell, a companion working or waiting, a browser's page, console errors. progress is 0 to 1 when there is a bar.
portstring The port's id; omit from a port's own page.
state.set(lines, port)
Say what a port is doing, in a few short lines, shown on its card when it is small (a peek), on its card under Running in the rail and in ⌘K, before what Port42 knows about it. From a port's page: port42.state.set([{label, value}, …]) for itself. From an agent: name the port. Only the port, its author, the person or a companion in its space may set it. Kept until the port sets it again or closes.
linesarray, required Up to 5 lines, first the most important, each {label, value}: e.g. [{"label":"doing","value":"building the join card"},{"label":"progress","value":"3 of 5"}]. Values are cut at 80 characters. An empty list clears it.portstring The port's id; omit from a port's own page.
storage
storage.delete(key, options)
Delete a value from persistent storage
keystring, required The storage key to deleteportstring For a copy of a port shared from another computer: that port's id, to reach its own storage there.scopestring "global" for storage shared across spaces; omit for this space's.sharedboolean true for the space's shared bucket rather than the caller's own.
storage.get(key, options)
Get a value from persistent key-value storage. Private to the caller by default; options {shared:true} and {scope:'global'} widen it, and {scope:'global', shared:true} is a PUBLIC board every caller on this computer can read and overwrite, so treat what you read there as untrusted.
keystring, required The storage keyportstring For a copy of a port shared from another computer: that port's id, to reach its own storage there.scopestring "global" for storage shared across spaces; omit for this space's.sharedboolean true for the space's shared bucket rather than the caller's own.
storage.list(options)
List all keys in persistent storage
portstring For a copy of a port shared from another computer: that port's id, to reach its own storage there.scopestring "global" for storage shared across spaces; omit for this space's.sharedboolean true for the space's shared bucket rather than the caller's own.
storage.set(key, value, options)
Store a value in persistent key-value storage. Private to the caller by default. {scope:'global', shared:true} is a PUBLIC board: every port, companion and client on this computer can read and overwrite it, so never store secrets or personal data there.
keystring, required The storage keyportstring For a copy of a port shared from another computer: that port's id, to reach its own storage there.scopestring "global" for storage shared across spaces; omit for this space's.sharedboolean true for the space's shared bucket rather than the caller's own.valuestring, required The value to store
terminal
terminal.exec(command, options)terminal
Execute a shell command and return the output. Runs in /bin/zsh.
commandstring, required The shell command to executecwdstring Working directory (default: home)timeoutinteger Timeout in seconds (default: 30, max: 120)
timer
timer.after(seconds, id)
From a port's page: call back once, after `seconds`, as port42.timer.after(seconds, fn). Paced as timer.every: only a paused port may fire it later, as soon as it is shown again. Returns { id }.
idstring The timer's id, chosen by the page (the port42 library does this); else Port42 makes one.secondsnumber, required Seconds between ticks (at least 0.25).
timer.cancel(id)
From a port's page: stop a timer, by the id timer.every or timer.after returned (port42.timer.cancel(id)). A port's timers also stop when it closes or its page reloads.
idstring, required The timer's id.
timer.every(seconds, id)
From a port's page: call back every `seconds`, as port42.timer.every(seconds, fn), which returns the timer's id. Use it instead of setInterval: Port42 owns the clock and runs it at full rate wherever the port is, another space included; only a port the person paused slows to about once a minute, and it fires at once when shown again. Returns { id }.
idstring The timer's id, chosen by the page (the port42 library does this); else Port42 makes one.secondsnumber, required Seconds between ticks (at least 0.25).
user
user.get()
Get the current user's identity (id and display name)
The source is MIT licensed and public on GitHub.
Roadmap
Port42's first commit landed on March 7, 2026, and 58 releases shipped before v1. Nautilus is Port42 v1.
Contributing
Port42 is open source under the MIT license, and contributions are welcome.
Bug fixes and small improvements
Open a pull request. Fork, branch, fix, and submit with a clear commit message.
New features and major changes
Major changes start with a Port42 Proposal (P42P) before any code is written. The port contract and the gateway API are shared by you, your AI companions, and every other program that calls them, so a companion that works today should still work tomorrow. A P42P covers the user flows, the architecture, acceptance criteria, changes to the contract, security implications, and an implementation plan with tests.
What counts as major: new user-facing features, changes to the port contract or the gateway API, architectural changes, and removing or changing existing behavior. The full guidelines and the P42P template are in CONTRIBUTING.md.