ctermdocs HomeBlogSign in

Remote access

Out of the box cterm only answers on the Mac itself, at http://127.0.0.1:7433/. This page covers putting it behind a password, letting other people sign in with accounts of their own, and reaching it from anywhere through an https address of your own. You need this for a phone, a laptop away from home, or a share link someone opens elsewhere.

The sign-in page: a username, a password, and the note that this browser stays signed in for 90 days of use.
The sign-in page: a username, a password, and the note that this browser stays signed in for 90 days of use. ui-login.png, drawn by login-test.mjs

What it is for

cterm is a terminal on your Mac. Anyone who reaches it can run Claude, and as an admin, shells, as your macOS user. So it has to stay closed to strangers while it's open to you from wherever you are. cterm does this without a cloud service or an account anywhere else:

  • A password of cterm's own, with optional accounts for other people.
  • A trusted device per browser you sign in from, which an admin can revoke from the settings view or the Mac.
  • An SSH reverse tunnel that the Mac opens to a server you control. The server already runs Caddy and gives cterm an https name. Nothing listens on the Mac for the outside world.

How to use it

Set a password first

On the Mac, run:

cterm auth set-password    # asks twice, without echo; at least 8 characters
cterm auth status          # password set or not, accounts, trusted devices, what the daemon enforces

Until a password is set, the app shows a banner: "No password set: only use cterm from this Mac." Once it's set, every browser is sent to the sign-in page. The Username field is optional while only one account can sign in. Sign in once and that browser stays signed in for 90 days of use. Each visit restarts the 90 days.

Give other people accounts

An admin can do everything. A user runs Claude sessions in their own projects under their own folder (by default ~/cterm/<name>), with no shell and no settings but their own machines: a user can run their sessions on a machine of theirs instead (Your own machine).

cterm user add marta                     # asks for her password; --role admin for a second admin
cterm user grant marta ~/projects/thesis  # she may also work in this existing folder
cterm user list                          # role, password, devices, sessions, folder root

An admin can do the same without a terminal from the settings view's Accounts card: + account, and each row's ⋯ menu for the role, the root, the password, the devices and removing it. Accounts need the first admin's password to be set. A user who signs in sees only their own projects and sessions. Accounts and projects covers the rest.

What a user account sees on first sign-in: no projects yet, an offer to make the first one, and no admin buttons.
What a user account sees on first sign-in: no projects yet, an offer to make the first one, and no admin buttons. ui-user.png, drawn by login-test.mjs

See and revoke trusted devices

cterm auth sessions          # every signed-in browser: id, account, device, created, last seen
cterm auth revoke <id>       # sign one device out
cterm auth revoke --all      # sign every device out

In the app, an admin sees each account's devices on its row of the Accounts card in the settings view, with when each was last seen, and signs one out with Revoke or all of them with Revoke all (on your own account, all but the browser you use). A device signed out there goes back to the login page at once.

The sign-out button in the sidebar's account foot signs out the browser you're using. Changing a password signs out every device of that account, and so does a revoke. Either one also drops that device's push subscriptions. For a script that needs to act as you, cterm auth issue --ttl 15m prints a cookie for a device that ends on its own (24 hours at most), so no password has to be stored in a file.

Open the tunnel

You need a server you can reach over SSH as root, already running Caddy on ports 80 and 443, and a DNS name pointing at it directly (no proxy in between). Then, on the Mac:

cterm tunnel setup cterm.example.com --server server.example.com
ssh root@server.example.com 'bash -s' < ~/.cterm/tunnel/server-setup.sh
cterm tunnel start
  • setup refuses to run without a password. It makes a key used only for this tunnel, pins the server's host key and prints its fingerprint to compare, and saves the settings. It sets publicOrigin to https://cterm.example.com and turns on trustProxy. Finally it writes the script the server needs.
  • Read the script before you run it. It adds three things: a system user that can only forward the one port, an SSH setting for that user alone, and one Caddy site. Each change is validated before a reload, nothing is restarted, and a second run changes nothing.
  • start loads the tunnel as a background agent on the Mac and waits until https://cterm.example.com/ answers.

Later, cterm tunnel status shows the settings, the agent's state and whether the public address answers. cterm tunnel stop closes it until the next login, and cterm tunnel remove forgets it on the Mac and prints what to remove on the server.

Or open it through Cloudflare

With a domain on Cloudflare, there's no server to keep: a Cloudflare Tunnel dials out from the Mac, and Cloudflare serves the https name. Make the tunnel on Cloudflare's side first (Zero Trust, Networks, Tunnels, or its API), with a public hostname such as app.example.com going to http://127.0.0.1:7433, and copy its token into a file. Then, with cloudflared installed (brew install cloudflared):

cterm tunnel cloudflare setup app.example.com --token-file ~/Downloads/tunnel-token
  • setup refuses to run without a password. It keeps the token in ~/.cterm/tunnel/cloudflared.token, readable by you alone; delete the file you gave it afterwards.
  • It adds https://app.example.com to the Origin allow list and turns on trustProxy. When no publicOrigin is set, the new name becomes it; otherwise the name is kept in otherOrigins, so the old address keeps working while you move. cterm config set publicOrigin https://app.example.com makes the move.
  • It starts cloudflared as a background agent and waits until https://app.example.com/ answers.

cterm tunnel cloudflare status, stop, start and remove work as they do for the SSH tunnel. Its log is ~/.cterm/logs/cloudflared.log.

Check the proxy settings

cterm config show
cterm config set publicOrigin https://cterm.example.com
cterm config set trustProxy true

cterm config set otherOrigins https://old.example.com keeps a second address allowed, the old one while you move to a new name. tunnel setup sets both, so you only need these for another kind of proxy. publicOrigin is the address browsers use, and share links are built on it. Turn trustProxy on only behind a proxy that sets the forwarded headers.

Show a landing page to visitors

cterm's own install at app.cterm.dev keeps this switch off: its public face is https://cterm.dev, a static site with the landing page, a blog and these pages, built from site/ in the repository and served by Cloudflare, so it stays up while the Mac sleeps. The switch below is for an install of your own that has no such site.

Someone who opens cterm's address without an account meets a bare sign-in form. cterm can show them a public page instead: what cterm is, how it works and what it does, with screenshots, and a Sign in button. It is off until an admin turns it on:

cterm config set landing on    # signed-out visitors at / get the page; off again: cterm config set landing off
cterm config show              # says whether it is on

Read it first: while it is off, an admin signed in can open /welcome/ to preview it, with a line on top saying it is a preview. Only the page, its styles and its pictures are public: the app, its API and these Help pages stay behind the sign-in. The screenshots show made-up projects and people, drawn by docs/spike/landing-shot.mjs, which refuses a picture that shows anything of your Mac. A link to it shared in a chat shows a preview with its title, a line about cterm and a picture.

The page stays light and leads to a second level: the features cterm's owner uses most (the steward, task sessions, the master chat and the master view) each have a public page of their own under /welcome/<name>/, with pictures and scenarios, linked from their section and from the bar on top. They follow the same switch, and a link to one shows its own preview.

A signed-in device still gets the app at /, and the app installed on a phone's home screen still opens on the sign-in form. The page answers only while the Mac is awake, like the rest of cterm.

The landing page's first picture: a session waiting for an answer, beside the sessions of three made-up projects.The landing page's first picture: a session waiting for an answer, beside the sessions of three made-up projects.
The landing page's first picture: a session waiting for an answer, beside the sessions of three made-up projects. ui-landing-session-dark.png and ui-landing-session-light.png, drawn by landing-shot.mjs

Keep a backup

The daemon makes a backup every night at 03:00 into ~/cterm-backups and keeps the last 14. A backup holds the store (accounts, password hashes, trusted devices, sessions list), config.json, the tunnel's keys, artifacts' copies and briefs. Admins see the last backup's age in Settings under Backups, with back up now. The age turns red after two days or after a failure.

  • cterm backup makes one now, cterm backup status and cterm backup list show what there is, and cterm backup set dir <folder> moves them, for example to an external disk.
  • cterm backup restore <file> puts one back while the daemon is stopped. What it replaces is moved aside, never deleted.
  • cterm backup remote setup can also send each archive, encrypted, to a server. Only an identity file you keep off the Mac can open those copies.

How it works

The daemon listens only on 127.0.0.1, on two ports. The public port (7433) serves the app, /api and the WebSocket. The hook port (7434) takes Claude Code's hooks and status line. A proxy only ever forwards the first, so nothing arriving through the tunnel can reach a hook. Each session also proves itself to the hook port with a secret of its own.

Traffic through a reverse tunnel arrives from loopback, so an address proves nothing. That's why the tunnel needs a password. The password is stored as an scrypt hash on the account's row in ~/.cterm/cterm.db. A wrong password waits 400 ms, and five failures from one address in 15 minutes (or 50 in all) are refused for a while. With trustProxy on, that address is the visitor's real one rather than the server's. A sign-in sets an HttpOnly, SameSite=Lax cookie, Secure over https. The store keeps only the cookie's hash, as a trusted device. Without the cookie, pages redirect to /login, /api answers 401 and the WebSocket is refused. The running daemon picks up cterm auth, cterm user and cterm config changes without a restart.

The Origin policy stops a web page elsewhere from using your signed-in browser against cterm. The WebSocket and every route that changes something refuse a browser Origin other than cterm's own (127.0.0.1, localhost), the dev server's, publicOrigin, one of otherOrigins, or one listed in CTERM_ALLOWED_ORIGINS. Requests with no Origin (curl, scripts) pass that check but still need the cookie.

The tunnel agent keeps one ssh -N -R connection alive, checked against the pinned host key. After a drop it retries with a growing pause (5 seconds up to 5 minutes). After a refused key it waits 10 minutes, so a server that bans repeated failed logins doesn't lock the Mac out. When the Mac sleeps, the server closes the connection, and the tunnel is back a few seconds after the Mac wakes. Its log is ~/.cterm/logs/tunnel.log.

What to keep in mind

  • Never open the tunnel without a password. cterm tunnel setup refuses to, and cterm auth disable refuses while other accounts can sign in.
  • TLS ends on the server. Whoever is root there can see cterm's traffic, so use a server you trust. Through Cloudflare, TLS ends at Cloudflare, which sees the traffic the same way.
  • One hostname, one computer. Never configure the same tunnel name on two running Macs. They would take turns holding the port.
  • Share links skip the sign-in by design. A driving link types into a session as you, so share only what you mean to, and revoke links when you're done (Sharing).
  • The backup archive is a secret. It holds the tunnel's private key and the password hashes. It's written readable by you alone. Keep the folder somewhere you trust.

Scenarios

Opening cterm to your phone

You have a small server running Caddy and a domain. You run cterm auth set-password, then cterm tunnel setup cterm.example.com --server server.example.com. You compare the fingerprint it prints with the one on the server, read server-setup.sh and run it over SSH, then cterm tunnel start. A minute later https://cterm.example.com/ asks for your password on your phone, and you add it to the home screen (cterm on a phone).

A lost laptop

Your laptop was stolen with cterm signed in. On the Mac, cterm auth sessions lists the devices. You find the one labelled with the laptop's browser and run cterm auth revoke with its id. Its next request is refused. To be thorough, you change the password with cterm auth set-password, which signs out every device, and sign in again on your phone.

A colleague with their own corner

A colleague wants Claude for their thesis, on your Mac. You run cterm user add marta and cterm user grant marta ~/projects/thesis. She signs in at the same https address with her username. She sees only her folder and her sessions, with no shell, no settings and none of your projects.

Screenshots on this page are drawn by docs/spike/login-test.mjs, docs/spike/landing-shot.mjs from fake data, and refreshed when a view changes.

Written in docs/site/pages/remote-access.md and built by docs/site/build.mjs.