Teleport an app

Bring an app from this computer into a Space: install it there, open your files in it, or move its signed-in state, with a consent screen for everything that leaves.

Teleport an app lists every app installed on this computer and says what teleport can do with each one in a Space:

LevelWhat happens in the SpaceMoves
Fullthe app is there, and a provider can import its signed-in statethe app; the app with files; the app with its state
Install onlythe app is installed from the pinned, checksum-verified install manifest, and opens empty or with your filesthe app; the app with files
Unsupportedshown disabled, with the reason (for example "not published for aarch64 Linux")nothing

Full today: firefox, chrome, slack, discord, vscode, notion, unity-hub, steam, whatsapp (macOS) and claude-code. Install only: Blender (x86_64). Apps come from /Applications and ~/Applications on macOS, .desktop entries on Linux and the Start Menu on Windows.

Before anything runs, the consent screen lists each install (with its pinned version and checksum), each host file or folder, and each state item. Secrets (cookies, tokens, logins) need an explicit acknowledgement, and on macOS the OS asks for Touch ID or your password before they are read.

For a browser (Chrome, Firefox), the app with its signed-in state moves the tabs, preferences and site data, but no credentials. Three separate opt-ins, each off until you tick it, add them: Keep me signed in (the session cookies), Saved passwords and Browsing history. In the SDK these are the entry's sensitive_groups and TeleportPlanOptions.sensitive_groups.

Chrome (and Chromium) encrypts cookies and saved passwords at rest with a per-computer Safe Storage key held in this Mac's login Keychain, so they cannot simply be copied to another computer. When Keep me signed in is checked, teleport decrypts the cookies here (the OS asks you to allow that Keychain read) and carries the decrypted values, not the source computer's key; the Space re-encrypts them under its own Safe Storage key before Chrome ever reads them. The source's key never leaves this Mac.

For Chrome, Saved passwords here still moves Login Data as an encrypted file, which only a Chrome that already shares this Mac's Safe Storage key can read -- a Space's Chrome does not, so a password moved this way does not yet decrypt there. (For Firefox, Saved passwords decrypts here with the profile's own key4.db master key and re-encrypts under the destination profile's own key, the same way as Keep me signed in, so it does decrypt there -- unless a master password is set, which is refused rather than guessed at.) To actually sign in with a saved Chrome password, use the Keyvault (cua keyvault import-passwords) instead: it decrypts on import and signs in through site login, without ever exposing the password to an agent. Keep me signed in (cookies) does not have this limitation on either browser.

Choosing sites and sending from the Keyvault#

In Cua Spaces, Keep me signed in lists the browser's sites with counts (for example github.com, 12 cookies, 3 storage values) so you send the sites you mean to, not the whole profile. It starts from what you sent to that Space last time, or from the sites that look like they keep a sign-in; an identity provider (Google, Microsoft, Apple, Okta) is never picked for you. Other lines of the consent screen can be turned off one by one. Cookies are read once to build the list's counts from the plaintext columns of Chrome's store; nothing is decrypted until you teleport.

If the Keyvault already holds items for the app (from Save to Keyvault), the review can send from the Keyvault instead: you pick from the saved items, nothing is read from this Mac, and macOS does not ask for Keychain access. See Keyvault.

In Cua Spaces#

  • Teleport an app… in a Space's window or viewer opens the picker: search, recent apps first, then what to move, then the consent screen and progress.
  • Drag an app from Finder or the Dock onto a Space tile or window: the picker opens with that app. Dropped files and folders still go to the Space's ~/Downloads.
  • Drag a window of any supported app: the notch shows Teleport to Cua with a preview of that window; drop it on a Space to teleport the app. This needs the Accessibility permission (macOS).

When you teleport a browser's signed-in sites, a Set up sheet comes before the consent screen if anything is missing or there is a choice to make. It shows the Keyvault, Full Disk Access, Automation and the browser profile, each with a plain word for where it stands and what to do about it. Full Disk Access needs the macOS setting turned on for Cua (the sheet opens System Settings for you), and then the Cua app opened again; without it you can still send open tabs only. When the browser has more than one profile, the sheet lists them with the account signed in to each and marks the one used last, and the profile you choose is the one that is read. Nothing is read, and Touch ID is not asked, until you continue past the sheet.

Files land in ~/Downloads/Teleported in the Space and open in the app.

Teleport an app: the apps on this Mac, narrowed to an aarch64 Linux Space

The consent screen: the pinned install and the folder that leaves this Mac

Visual Studio Code open in the Space with the teleported folder

Where it runs#

App teleport runs in the Cua Spaces apps and in the cua that ships with them (cua teleport, below). It is part of Cua Spaces, source-available under FSL-1.1-MIT, and not in the open source SDK packages: SDK programs in Python, TypeScript, Swift or Kotlin reach session teleport through Space.teleport against the Cua Spaces daemon (below).

To build your own Spaces UI in Swift, the Cua Spaces app export (CuaSpacesFFI, reference) has the calls the apps use: Teleport.catalog, plan and run.

import CuaSDK
import CuaSpacesFFI
 
let tp = cua.teleport()
let options = try await tp.spaceHint(space: space)   // the Space's OS and CPU narrow the catalog
let apps = try await tp.catalog(options: options)
let firefox = apps.first { $0.id == "firefox" }!
let plan = try await tp.plan(app: firefox, space: space, options: TeleportPlanOptions(
    moves: .appWithFiles, files: [notes], launch: false))
for item in plan.consent { print(item.label, item.detail) }   // show these to the user
let report = try await tp.run(plan: plan, space: space,
    consent: TeleportConsent(approved: true), listener: nil)
print(report.sent)

run refuses a plan without approved, and a plan with secrets without acknowledgeSensitive. Pass a TeleportRunListener for progress events. TeleportAppPicker from CuaSpacesTeleport is the picker the macOS app uses. Drops go through Teleport.parseDrop, window drags through Teleport.startWindowDrag (macOS; Linux and Windows return Unsupported). For a web UI, @trycua/cua/teleport has the headless picker state machine, <cua-teleport-picker> and <cua-teleport-drop> web components and drag-payload helpers over a TeleportHost you provide.

The cua keychain on macOS Spaces#

Chrome and other Chromium browsers keep their cookie key ("Safe Storage") in the Keychain. Session teleport installs that key into a keychain that cua-spacesd owns, ~/Library/Keychains/cua.keychain-db, and pre-authorizes the browser there. It never reads, writes or deletes anything in the Space's login keychain, so no password dialog ever appears in a Space nobody is watching, whether or not the browser was opened in the Space first:

  • The login keychain of a Lume image can have a password nobody knows (the image's autologin file is padded wrong, so macOS re-keys the keychain at every boot), and an item a browser made there trusts only that browser. Reading it with /usr/bin/security raises "security wants to use your confidential information stored in 'Chrome Safe Storage'" with nobody to answer it.
  • The cua keychain is created with a password the Space knows and is the first entry on the keychain search list. Browsers look their key up through the search list and take the first match, so Chrome finds the key teleport installed before any key it made itself. That key is left alone, not read and not deleted. Cookies Chrome stored in the Space under its own key before the teleport do not survive it; the teleport replaces the session.
  • It is not the default keychain, so new items are not written there by other apps.
  • cua-spacesd unlocks it when it starts and again every 20 seconds, so it is open after a reboot and within seconds of a wake, and puts it back first on the search list if something reset it. The password comes from the Space's protected configuration (CUA_ENV_LOGIN_KEYCHAIN_PW, ~/.cua/spacesd/keychain-password, /etc/cua/keychain-password, then the image's account password).
  • It never auto-locks: no lock after a timeout and none on sleep (security set-keychain-settings without -l or -u).
  • Every security call has a 20 second deadline. A call that is stuck behind a dialog is stopped, the dialog is dismissed, and the teleport fails with an error that says so instead of hanging.
  • A running Chrome in the Space is quit before its profile is replaced and opened again afterwards, so it reads the key teleport installed.
  • A Space that was never teleported into has no cua keychain.

Session teleport#

To move only an app's signed-in state (scope tabs or full) into a Space, with an approval callback. Space.teleport asks the Cua Spaces daemon, which runs teleport; an SDK runtime without Cua Spaces raises HostCapabilityMissing.

import cua
 
class AskUser(cua.TeleportApprover):
    def approve(self, manifest):
        print([(i.relative_path, i.is_sensitive) for i in manifest.items])
        # include=None: the default selection. A list: exactly those items.
        return cua.TeleportDecision(include=None, acknowledge_sensitive=False)
 
receipt = await space.teleport("firefox", "tabs", AskUser())
print(receipt.transferred_paths, receipt.launched)

The Rust tab is from the examples in other languages; the Swift tab is from Cua Bots, which teleports a saved sign-in the user picked. Returning nothing raises TeleportRefused. Sensitive items need acknowledge_sensitive=True. MCP tools: teleport_manifest, teleport_app.

From the CLI. cua teleport runs in the cua that ships with the Cua Spaces apps; a cua installed on its own hands the command to that one, or fails with an error that says where it ships.

cua teleport providers                          # apps this machine can teleport
cua teleport manifest --app firefox --scope tabs
cua teleport push --app firefox --sandbox dev
cua teleport push --app com.google.Chrome --url http://10.0.0.5:3211 --token "$TOKEN" \
  --profile "Profile 1" --progress

push is your approval and prints what it sends and leaves behind. --include PATH picks items, --all sends everything, --no-launch imports only. --sandbox NAME routes through your own Cua Keyvault (one combined Touch ID prompt for the whole capture-and-deliver, and nothing raw ever leaves it). --url names a destination that is not one of your Spaces, such as another computer's address, and goes the same way: the Cua daemon on this computer reads the app, you approve it with the same Keyvault prompt, it is recorded in the audit log, and the copy is wiped on the same schedule. The destination is only where it goes, and it is not kept in your Spaces afterwards. Because the daemon does the reading, macOS uses the Full Disk Access you gave the Cua app, not the permissions of the terminal you ran the command in.

When macOS asks for permission#

Sending a browser's signed-in sites reads its files, and macOS decides whether Cua may do that. Teleport checks first and says what is missing, in words, before it asks for Touch ID or reads anything:

cua teleport doctor
cua teleport doctor --app chrome --profile "Profile 1"

It reports whether the Keyvault is set up, whether macOS lets Cua read the browser's files (Full Disk Access) and its open tabs (Automation), and which browser profiles exist, with the account signed in to each and the one used last. Cua cannot grant these for you: macOS only lets you, in System Settings, under Privacy and Security. macOS applies Full Disk Access to a program when it starts, so after turning it on, restart the Cua daemon. Without Full Disk Access you can still teleport open tabs only. Without a --profile, a push reads the Default profile, so pick the one you are signed in to.

A teleported session is a live sign-in that anyone controlling the Space can use. Teleport only into Spaces you own, and prefer tabs.