ribo CLI/Accounts & Profiles

Accounts & Profiles

If you only ever work with one Tissue account, ribo login is all you need and you can skip this page.

If you work with more than one account — a personal account and a client's, a company account and a demo account — or if you run AI agents that deploy on your behalf, read on. A single global login is a shared mutable variable, and this page is about not tripping over it.


The problem

ribo login stores one token in ~/.config/ribo/token.json, and the account you are acting as lives inside that token. ribo account switch re-mints it in place. That means account selection is global:

# terminal 1                        # terminal 2
ribo account switch "Acme"
                                    ribo account switch "Personal"
ribo deploy     # ← deploys as Personal, not Acme

Nothing errors. The deploy succeeds — into the wrong account. And because ribo deploy looks a Cell up by name within the token's account, deploying to the wrong account doesn't update your existing Cell: it creates a brand-new one there, with a live URL under the wrong subdomain and bindings pointing at the wrong account's databases and buckets.

Two shells is the mild version. Two coding agents on the same machine, working in different projects, is the version that bites.

Tissue gives you two independent defences. Use both:

  • Profiles bind a credential to a directory tree, so concurrent work stops fighting over one file.
  • The account pin records the account a project belongs to and aborts any deploy that would land elsewhere.

Profiles: one credential per project tree

A profile is a named login bound to a directory. Any command run in that directory — or any subdirectory — authenticates as that profile.

Create profiles

If you're already logged in, capture the current login under a name:

ribo account switch "Acme Corp"
ribo profile create acme --from-current

ribo account switch "Personal"
ribo profile create personal --from-current

Or log in fresh, straight into a profile:

ribo profile create acme          # opens the browser, stores under "acme"
ribo profile create ci --key tok_a1b2c3...   # or store an API token

Bind them to directories

cd ~/work/acme-site   && ribo profile activate acme .
cd ~/personal/my-blog && ribo profile activate personal .

That's it. From now on, anything you run under ~/work/acme-site is Acme, and anything under ~/personal/my-blog is you — at the same time, in as many terminals or agents as you like. Neither can clobber the other, because they no longer share a credential.

Check before you act

$ cd ~/work/acme-site
$ ribo profile which
directory  /Users/you/work/acme-site
credential profile "acme" (bound at /Users/you/work/acme-site)
server     https://api.tissue.systems
account    Acme Corp (acct_3f8c1a2b4d6e8f00)

ribo profile which has no side effects and answers the one question that is otherwise invisible: who am I about to be? It's the right thing to put at the top of a deploy script, and the right thing to teach an agent to run before it touches production.

ribo deploy reports the same thing on every deploy, as a via line:

account   Acme Corp (acct_3f8c1a2b4d6e8f00) [pinned]
via       profile "acme" (bound at /Users/you/work/acme-site)
server    https://api.tissue.systems
deployed  acme-site

See everything at once

$ ribo profile list
* acme
    account   Acme Corp (acct_3f8c1a2b4d6e8f00)
    servers   https://auth.tissue.systems
    bound     /Users/you/work/acme-site

  personal
    account   Personal (acct_9zp5k1m3n5p7r9t1)
    servers   https://auth.tissue.systems
    bound     /Users/you/personal/my-blog

Here (/Users/you/work/acme-site): profile "acme" (bound at /Users/you/work/acme-site)

Which credential wins

ribo picks a credential in this order — the first one that applies:

# Source Scope
1 RIBO_TOKEN environment variable the process
2 --profile <name> one command
3 the nearest bound ancestor directory that directory tree
4 the default profile (ribo login) everywhere else

Nearest wins. If ~/work is bound to outer and ~/work/acme is bound to acme, then ~/work/acme/site uses acme — the most specific binding, not the first one found.

--profile works on any command, and it redirects writes as well as reads. ribo --profile acme account switch "Acme Staging" re-mints the credential inside the acme profile and leaves everything else alone.

A stale binding is an error, not a fallback. If a directory is bound to a profile you deleted:

Error: /Users/you/work/acme-site is bound to profile "acme", which no longer exists.

  Re-create it:  ribo profile create acme
  Or unbind it:  ribo profile deactivate /Users/you/work/acme-site

ribo will not quietly fall back to your default login, because quietly retargeting a deploy at a different account is precisely the failure profiles exist to prevent.


Pin the account in ribo.toml

Profiles make the right credential ambient. The pin makes a wrong one fatal.

cd ~/work/acme-site
ribo account pin
# Pinned Acme Corp (acct_3f8c1a2b4d6e8f00) in ribo.toml.

That writes an account_id into the [cell] block:

[cell]
name = "acme-site"
account_id = "acct_3f8c1a2b4d6e8f00"

The pin is an assertion, not a selector. It never picks an account for you — it only stops a deploy that would land in the wrong one, before anything is uploaded:

Error: Refusing to deploy: account mismatch.

  ribo.toml pins   Acme Corp (acct_3f8c1a2b4d6e8f00)
  active account   Personal  (acct_9zp5k1m3n5p7r9t1)

  Switch accounts:                 ribo account switch "Acme Corp"
  Or use a profile for this tree:  ribo profile activate <name> .

The check runs on both sides: ribo refuses locally, and the deploy request carries the expected account id so the server refuses too. An out-of-date CLI can't get around it.

Commit the pin. It contains no secret — an account id is not a credential — and it means anyone who clones the project, including an agent, gets the guardrail for free.

Don't pin public templates. If you publish a project for other people to clone and deploy into their accounts, leave it unpinned; a pin would fail every one of their deploys with an account id that means nothing to them.


Recipes

Two agents, two accounts, one machine

# one-time
ribo profile create acme --from-current       # after: ribo account switch "Acme Corp"
ribo profile create personal --from-current   # after: ribo account switch "Personal"
ribo profile activate acme     ~/work/acme-site
ribo profile activate personal ~/personal/my-blog
(cd ~/work/acme-site   && ribo account pin)
(cd ~/personal/my-blog && ribo account pin)

Then both agents can run ribo profile which and ribo deploy concurrently, in any order, with no coordination.

CI

Nothing to change. Set RIBO_TOKEN to a scoped API token and profiles never come into play — an environment variable is already per-process, so parallel jobs on one runner are isolated by construction:

- name: Deploy Cell
  env:
    RIBO_TOKEN: ${{ secrets.RIBO_TOKEN }}
  run: ribo deploy

ribo profile which tells you when RIBO_TOKEN is in play, so an environment credential shadowing a profile is never a silent surprise.

Retiring a profile

ribo profile deactivate ~/work/acme-site   # unbind one directory
ribo profile delete acme                   # remove the profile and unbind everything

Where credentials live

~/.config/ribo/
├── token.json      ← your default login (ribo login)
└── profiles.json   ← named profiles and their directory bindings

Same path on every platform, macOS included. Both files hold bearer tokens, so ribo writes the directory as 0700 and the files as 0600, and re-applies those permissions on every write — including to files created by older versions of ribo. Treat them like an SSH private key: don't commit them, don't sync them into a shared volume.

Deleting profiles.json removes every profile and binding but leaves your default login intact.


A note on --from-current

ribo profile create <name> --from-current copies the credentials in your default login for every server it holds — production, staging, a local dev server. ribo account switch only re-mints the credential for the server it targeted, so the others are whatever they already were, and are not guaranteed to belong to the account you just captured.

When more than one server is copied, ribo says so and tells you how to check:

ribo --profile acme account whoami --server https://api.staging.tissue.systems
ribo --profile acme account switch "Acme Corp" --server https://api.staging.tissue.systems

If you only use production, there is nothing to do.


See also