passport-bridge
v0.16.8
Published
Passport CLI for governed terminal and CI agents, plus the local stdio-to-cloud MCP bridge.
Maintainers
Readme
Passport CLI and bridge
This package installs two binaries that reach a Passport workspace through its governed gateway:
passport: CLI for terminal agents, scripts, and CI.passport-bridge: stdio bridge for agents that launch a local MCP server, so the agent config holds no long-lived token.
Member profiles get the same tool access, agent selection, guardrails, approvals, and activity logging as native agents. Agent keys keep their pass, selection, guardrails, and audit trail but never raise a member approval prompt.
Install
npm install --global passport-bridgeRequires Node.js 22.23.2 or newer. npx passport-bridge@latest scan and npx passport-bridge@latest init need no install and run on Node.js 20 or newer; scan also needs no account. @latest keeps an older global install from running instead. The CLI makes no background registry request, telemetry, or self-update; check with npm view passport-bridge version and upgrade with npm install --global passport-bridge@latest.
Quick start
New here? One command sets everything up:
npx passport-bridge@latest initIt scans for keys your agents can reach, signs you in (or up), connects the apps it found, shows the environments Passport placed, adds Passport to your agents, turns on the guard, and sets Autopilot: how much agents can do on their own. See Set up with init.
Or step by step:
passport login
passport status
passport search "open issues"
passport describe github_search_issues
passport run github_search_issues '{"query":"SSO"}'- First time? Bare
passport loginopens/cliin your browser. Sign in with Google, GitHub, or an emailed code, then connect a workspace you belong to or create one. No workspace id needed. - Confirm that the code in the browser matches the one in your terminal. Nothing connects until you click Connect.
- Re-signing in? Bare
passport loginreuses the workspace of the active profile, the sole CLI profile, or the one workspace all saved profiles share. With several workspaces saved it lists them and asks for--workspace. --workspace ws_examplesigns in to one known workspace (the device flow). Self-hosted: add--cloud https://passport.example.com(origin only). A server without browser sign-in keeps asking for--workspace.--no-open(orPASSPORT_NO_OPEN=1) prints the sign-in link instead of opening a browser. Use it over SSH and in headless sessions; open the link on any device.- Remote servers must use HTTPS. Plain HTTP works only on
localhost,127.0.0.0/8, or::1. - Requires a gateway reporting serverInfo
passport0.5.0 or newer with the four-tool CLI protocol; older servers fail before any tool call. passport help [command]documents every option.help,version,profiles,use, andcompletionwork offline.
Commands
| Command | Purpose |
| --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| init [--yes] [--move-keys] [--no-open] [--cloud <origin>] [--skip-apps\|--skip-agents\|--skip-guard] [--verbose] [--json] | Set up Passport for this computer's agents in one pass. Every step asks first, can be skipped, and is safe to run again. |
| login | Browser sign-in or sign-up (or device sign-in with --workspace); saves a rotating CLI credential and makes the profile active. |
| logout | Revoke the credential and always remove the local profile (warns if revocation fails). |
| profiles / use <name> | List stored profiles / select the active one. Offline. |
| status | Workspace, member, agent, and app connection states, plus Autopilot presets, team floors, and how old this computer's copy of your rules is. |
| doctor | Check Node, credential-file safety, HTTPS policy, /healthz, auth, serverInfo, and CLI protocol. Exit 1 on a failed check, with the full report. |
| ready | Readiness preflight for automation; gate on its ready field and exit code. |
| search [query] [--app <key>] | Search permitted tools. Empty query lists the full catalog. |
| describe <tool> | One exact definition with its full parameter schema. |
| run <tool> [json] | Run a tool through Passport governance. |
| connect [app...] [--alias <name>] [--open] [--wait <s>] | Show member sign-in links and optionally wait for completion. |
| resources [uri] [--out <file>] | List or read MCP resources. |
| prompts [name] [--set k=v] [--out <file>] | List or get MCP prompts. |
| scan [--path <dir>] [--verbose] [--check] | Find keys your agents can reach on this computer and what each can do. Local only, no account. --check scans files only (not the shell environment) and exits 5 on a high or medium key. |
| discover | Scan local MCP configuration and submit a privacy-minimized fleet report. Exit 5 on a managed-policy violation. |
| hook | Records the CLIs agents run and, with --guard, decides each one before it runs from your Autopilot rules (install, uninstall, status). |
| wait <approval-id> [--timeout <s>] | Wait for a person to decide an approval request (default 100 seconds). |
| rules test "<command>" [--environment <kind>] | What Autopilot decides for a command, why, and which environment it acts on. |
| sessions [--status running\|idle\|ended] | Your agents' recent sessions with what Passport decided. |
| undo <action-id>\|--last [--option <id>] [--yes] | Undo an action Passport took a backup before, within 7 days. See Undo. |
| setup --org <pek_key> [--yes] [--agents <list>] [--lock [--strict]] | Set up every agent on this computer for your company without asking anything (MDM and scripts); --lock (root) locks the guard on. See Company rollout. |
| setup | Install and maintain assigned team workflows as managed skills. |
| session | Run NDJSON commands over one authenticated MCP session. |
| access | Signed managed access: keygen, actions, run, invoke, status, cancel. |
| completion <bash\|zsh\|fish> | Print a completion script. Offline. |
| help [command], version | Help; package version and local profile context. |
Global options: --profile <name>, --json, --timeout <seconds> (default 30), --quiet, --caller <id>.
Tool arguments
Use exactly one form:
passport run github_search_issues '{"query":"SSO","limit":10}'
printf '%s' '{"query":"SSO"}' | passport run github_search_issues --args -
passport run github_search_issues --set query=SSO --set-json limit=10--set values are strings; --set-json takes any JSON value. Both repeat.
Approvals and connections
passport run github_create_issue '{"title":"Customer report"}' --wait 90
passport connect github --wait 120- An approval-required call exits 4 with its approval ID.
--wait <s>retries only after a definiteapproval_pendingresponse. - The CLI never retries after a network failure or timeout, since the outcome is unknown.
connectwith no app names uses one aggregate link when several apps need sign-in. Agent keys get guidance but cannot create sign-in links.
Resources and prompts
passport resources passport+github://welcome
passport resources passport+github://image --out image.png
passport prompts github_summarize --set note=welcomeHuman mode prints text content. Binary content needs --out <file> or --json.
Key scan
npx passport-bridge@latest scan # or: passport scan
passport scan --path ../api --verbose
passport scan --check # CI and pre-commit: files only, exit 5 on a high or medium keyReads agent MCP configs (Claude Desktop, Claude Code, Cursor, VS Code, Codex), CLI sign-in files (GitHub CLI, Railway, Vercel, Wrangler, AWS, Stripe CLI, Supabase, npm), well-known shell variable names, and .env* files up to three folders deep in the project. It never prints or saves any part of a key, opens no network connection, needs no profile, and skips .env files in your home folder. Sign-ins held by the OS keychain are named but not flagged. passport-bridge scan and passport-bridge init run the passport CLI's own implementation.
Set up with init
npx passport-bridge@latest init # or: passport init
npx passport-bridge@latest init --yes # accept every default; needed without a terminal. Keys stay put
npx passport-bridge@latest init --yes --move-keys # also move CLI keys into Passport and sign those CLIs out
npx passport-bridge@latest init --yes --json # one summary object with each step's statusEight steps, each reusing the command that owns it:
Scan: the same local scan as
passport scan, as one summary line (--verboseprints the table).Sign in: reuses the active or only CLI profile, asks which one when there are several, or runs the browser sign-in from
passport login.Apps: Railway, Vercel, Supabase, Cloudflare, GitHub, and Stripe, when the scan found a key or its CLI is on PATH. Missing apps are added through the same catalog rules as the web (an app your workspace reviews first says
! Stripe needs approval from your admin.), then one sign-in page covers the rest. In a terminal init waits until they connect (Enter stops waiting); without one it prints the page andNext: passport connect --wait 300and moves on.Keys: for each CLI sign-in on this computer, the same question as
passport keys move:Store your Railway account token in Passport and sign the Railway CLI out on this computer? [y/n]. It has no default: Enter asks again (Type y or n.).--yesnever moves a key: the step printsKeys stayed on this computer. Run passport keys move to move them.Add--move-keysto move every key it finds unasked; the closing summary then says which (Moved your Railway key into Passport and signed that CLI out on this computer.) and which old keys to rotate (Rotate the old Railway key once your agents don't need it., only when a key moved in that run). When Passport already holds a different key for that CLI, an interactive run asksReplace the Railway key in Passport with the one on this computer? [y/n](no default): yes replaces it and signs the CLI out, keeping links; no printsKept the key in Passport. This computer's Railway CLI stays signed in.--yesnever replaces a key. Quiet when there's nothing to move.Environments: the environments Passport has placed for your apps, each with its kind:
Found 3 environments: production (Railway), preview (Vercel), qa (Railway). Anything Passport can't place counts as Production; change it any time in Rules. With no app connected yet it saysEnvironments appear once an app is connected.Agents: Passport's one MCP URL with this workspace as its hint (
https://passportmcp.com/mcp?ws=<workspace>), with each agent's own browser sign-in; a server that doesn't serve/mcpyet (read from its OAuth protected-resource metadata) gets the workspace's own URL (https://<host>/w/<workspace>/mcp). An existing entry at either URL for this workspace counts as set up, so no token lands in a config file. Claude Code and Codex through their own CLIs (claude mcp add --transport http --scope user,codex mcp add --url), Cursor through~/.cursor/mcp.json, VS Code throughcode --add-mcpor its usermcp.json. Claude Desktop connects through Settings → Connectors, so init prints that step.Guard: asks
Apply your Autopilot rules before each shell command in <agents>? [Y/n], then runspassport hook install --guardfor Claude Code, Codex, and Cursor (Codex: blocks only), pointing at the launcher$PASSPORT_HOME/cli/bin/passport(passport.cmdon Windows), which runs a stable copy of the CLI in$PASSPORT_HOME/cli/<version>/(never npm's cache), or at a globalpassport-bridgeof the same version or newer. An upgrade rewrites the launcher and thecli/currentlink in one rename each, so hooks follow without being edited; the launcher runs the recorded node, or the node on PATH once that one is removed. A hook from an older init that names a version folder is replaced where it stands, next to your other hooks. Once the new copy is in place, older copies are deleted unless a hook still runs them.Autopilot: how much agents can do on their own, per environment:
Autopilot How much can agents do on their own? Production Careful reads run, changes ask Staging Balanced reads and changes run, deletes ask Development Hands-off everything except irreversible runs Keep these? [Y/n]nchooses per environment;--yeskeeps the defaults. A team floor stricter than a default is shown with why (Your team requires Careful or stricter in Staging.), and choices below it aren't offered. A CLI credential can only make presets stricter; a looser pick says so in one line and keeps the current ones. A re-run shows the presets(already set up)without asking. The closing summary says what will ask:Autopilot on. You'll be asked only for Production changes, deletes outside Development, and irreversible actions.A server without Autopilot gets the earlier Safety step instead.
What changes on disk, and how to undo it:
| Where | What | Undo |
| ------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | ------------------------------------------------- |
| ~/.passport/credentials.json | A cli@<workspace> profile, as passport login writes | passport logout |
| ~/.passport/cli/ | A copy of this CLI, the bin/passport launchers hooks run, and a current link | Delete the folder (after removing the hooks) |
| ~/.claude.json | A passport MCP server entry, via claude mcp add | claude mcp remove --scope user passport |
| ~/.codex/config.toml | [mcp_servers.passport], via codex mcp add | codex mcp remove passport |
| ~/.cursor/mcp.json | mcpServers.passport | Delete that entry |
| VS Code user mcp.json | servers.passport | MCP: List Servers, then remove it |
| ~/.claude/settings.json, ~/.codex/hooks.json, ~/.cursor/hooks.json | Passport's audit and guard hooks | passport hook uninstall --agent <agent> --apply |
Before editing a file it didn't create, init says which file and keeps one copy of the original as <file>.passport-backup. It changes only Passport's own entries, writes atomically, refuses a malformed file with one line, and leaves an existing Passport entry alone unless it points at npm's cache or a file that's gone. Ctrl-C at any point stops cleanly; run init again to pick up where it left off. A question only takes keys typed after it appears, so an Enter pressed during a pause never answers it. Any wait longer than a moment shows what it's for in the step's row (Sign in Checking your sign-in…); off a terminal nothing extra prints. If Passport can't be reached, init says Couldn't reach Passport at <origin>. Check your connection, then run passport init again. and exits 1. Without a terminal, init needs --yes (exit 6 otherwise), and --json needs --yes. It never edits your shell profile: it prints the one PATH line to add if you want passport everywhere.
Company rollout
npx passport-bridge@latest setup --org pek_example --yes # Jamf, Kandji, Intune, or any script
npx passport-bridge@latest setup --org pek_example --agents claude-code,codexinit for MDM: it never asks anything and never signs in. It installs the stable copy of the CLI, adds Passport to every detected agent (Claude Code, Cursor, Codex, VS Code; Claude Desktop gets the connector step) at the one URL with the enrollment key as its hint (https://passportmcp.com/mcp?enroll=pek_…), turns on the guard and audit hooks for Claude Code, Codex, and Cursor, and records the key in $PASSPORT_HOME/enrollment.json (mode 600). It prints one line per agent:
Passport setup for passportmcp.com (enrollment pek_…mple)
Claude Code ✓ Passport added · guard on
Cursor ✓ Passport already set up · guard on
Codex ✓ Passport added · sign in: codex mcp login passport · guard on
Each agent asks you to sign in to Passport the first time it connects.The first line names the workspace the key pre-selects (GET /api/auth/enrollment/<key>, public, no credential). It exits 0 even with no agents (No supported agents found. Nothing to set up.), so an MDM run never fails a computer without them. A malformed key or --agents list (--clients still works) exits 6, and so does a key the server doesn't know or has revoked, with the server's one line (This enrollment key isn't valid. or This enrollment key was revoked. Ask your admin for a new one.) before anything changes. passport login on that computer later passes the recorded key: as the browser sign-in hint (POST /api/auth/cli/start { enroll }) and for rollout attribution when the CLI credential is minted (enrollmentKey). A server that doesn't serve the one URL yet still gets the CLI, hooks, and key, and the agents wait for passport init. Run it again any time: it changes nothing that's in place and repairs what drifted (a removed hook, an entry pointing at npm's cache). The enrollment key is a workspace hint, not a credential: people still sign in as themselves. --cloud <origin> (or PASSPORT_CLOUD) points it at a self-hosted Passport. --json prints {version,origin,universal,url?,workspace?:{id,name},enrollment,clients:[{agent,name,mcp,guard,message?,next?}]}.
sudo npx passport-bridge@latest setup --org pek_example --yes --lock # lock the guard on
sudo npx passport-bridge@latest setup --org pek_example --yes --lock --strict # and allow only managed hooks--lock, run as root, puts the guard and audit hooks in each agent's managed settings instead of the per-person steps (as root those would write into root's home), so people can't turn them off (src/cliSetupLock.ts, src/lockedHooks.ts, a checked copy of shared/lockedHooks.ts). It writes a root-owned launcher (/Library/Application Support/Passport/bin/passport on macOS, /opt/passport/bin/passport on Linux, 0755) that sets PASSPORT_HOOK_VIA=managed and runs the person's own $PASSPORT_HOME/cli/bin/passport; without one it lets the command run, or with --strict blocks it with Passport's guard isn't set up for you yet. Run: npx passport-bridge@latest init. Then per agent: Claude Code gets the drop-in managed-settings.d/50-passport.json; Codex's /etc/codex/requirements.toml gets Passport's [features]/[hooks] lines between # >>> passport and # <<< passport; Cursor's system hooks.json gets Passport's two entries beside everyone else's. --strict adds allowManagedHooksOnly and onFailure: "block" (Claude Code), allow_managed_hooks_only = true (Codex), and failClosed (Cursor). A file it can't merge safely (someone else's drop-in or [hooks], hooks = false, disableAllHooks, invalid JSON) is left untouched with the fix. Atomic writes, files 0644. Without root, or on Windows, it writes nothing and prints Locking needs administrator rights. Run it with sudo, or deploy these through MDM: with each path. --clients narrows it; --strict without --lock exits 6. It exits 0 and prints one line per agent (Claude Code Locked · <path>, Already locked, or ! Not locked: <reason>); --json prints {version,lock:{strict,shim,agents:[{agent,state,path,message?}]}} with state one of locked, already_locked, conflict, needs_admin, failed. A hook from the person's own settings stands down when managed settings already run Passport's guard (src/managedHooks.ts). PASSPORT_MANAGED_ROOT puts every managed path under a folder (tests). Vendor docs: code.claude.com/docs/en/settings, code.claude.com/docs/en/hooks, learn.chatgpt.com/docs/enterprise/managed-configuration, cursor.com/docs/agent/hooks.
Team workflows
Codex and Claude Code on macOS and Linux. Requires a member profile.
passport setup list
passport setup install <setup-id> <workflow-key> --agent codex # preview
passport setup install <setup-id> <workflow-key> --agent codex --apply
passport setup installed
passport setup rollback <skill-name> --apply
passport setup remove <skill-name> --applyChanges preview unless --apply is passed. Personal edits are preserved. installed, remove, and rollback work offline. Installing a workflow never grants app permissions or provisions accounts.
Shell hook
passport hook install --agent claude-code --guard --apply # or codex, cursor
passport hook status
passport hook uninstall --agent claude-code --guard --apply # guard onlyTwo hooks per agent. passport hook runs after each shell command and records the vendor, a coarse verb, a risk class, and (with Autopilot) the environment name the flags gave and the guard's decision in Activity; the server works out the class and cause. passport hook guard runs before it and decides from the member's Autopilot rules (src/cliHookGuard.ts):
- Allow (Claude Code
permissionDecision: "allow", Cursorpermission: "allow") only when every part of the line is understood and allowed: vendor commands insrc/cliHookVendors.tsread with confidence, and everyday commands (below). Substitutions, heredocs, variables in a path, unknown wrappers or environment prefixes, scripts, aliases,make, and programs Passport doesn't know mean no decision, so the agent's own permission rules apply. Codex can't receive allow; its own approvals apply (seepassport sync). - Ask or deny when any recognized command needs it. The strictest part of a compound command wins and names itself.
--helpand--dry-runnever ask. - Each reason names its cause in one line:
Passport: railway redeploy changes Production. Approve to run it., orBlocked by Passport: Team rule, no deletes in Production. Ask an admin to run it, or request an exception in Passport.
Speed. Hooks must never slow agents down. The launcher $PASSPORT_HOME/cli/bin/passport (written by src/initInstall.ts) answers plain reads and local dev commands in POSIX sh without starting Node (src/launcherFastPath.ts): ls, cat, rg, git status, cargo check, and the rest of a fixed list, joined only by single pipes, with relative paths that stay in the folder and pass through no symlink or hidden file. It answers only while cli-policy/fastpath.ok is armed: a full guard run arms it for five minutes when the cached rules give those commands exactly the launcher's answer (src/hookFastPath.ts), and a policy or profile change removes it. Each answer appends one line (agent, session id, program names, never an argument) to cli-policy/fastpath.spool, which the next full run counts. Anything else, or any doubt, starts Node with the same input, through dist/hookMain.js, which loads only the guard. src/launcherFastPath.test.ts holds the launcher to the full guard's exact answer across both corpora and generated variants. PASSPORT_HOOK_FAST=0 turns it off. The after-hook and the guard's own report never wait on the network: they queue to $PASSPORT_HOME/hook-outbox/ and a detached passport hook flush sends them within the budget (src/hookOutbox.ts). Each guard run records its timings (numbers only, src/cliSpeed.ts), sent every five minutes with the answered counts. npm run bench:guard times it all on the compiled CLI.
Script reading (src/cliScriptRead.ts, src/cliScriptText.ts). For the project's own code, ./x.sh, bash x.sh, source x.sh, make <targets> (prerequisites first, variables resolved once), and npm|pnpm|yarn run <name> or npm test (with pre/post scripts), the guard reads the file and classifies every line like a typed command; the strictest finding wins and says where it came from (railway up changes Production (from --environment, in deploy.sh); Sessions shows · from deploy.sh). A line it can't follow makes that part Partly checked: no allow, reported as unchecked, with a hash (opaqueIds) instead of its name, keyed with 32 random bytes made on the computer and kept in $PASSPORT_HOME/opaque-salt (0600), never sent; suggestion counts are therefore per computer. Limits: 256 KB a file, 20 files and 3 levels a command, 30 ms a call. Results are cached in $PASSPORT_HOME/scan-cache.json (0600) by path, mtime, and size, then SHA-256; findings only. The after-hook reads it again and reports drift when it changed in between. Package scripts no longer take the launcher's fast path, since Node reads them. With a warm cache, a 60-line script adds a few milliseconds; bench:guard --check fails over 20 ms.
Everyday commands (src/cliHookLocal.ts, tables in src/localCommands.ts, localTools.ts, localFiles.ts, localGit.ts, localNetwork.ts). Local commands are placed by what they touch on this computer. The project folder (the git root, else the agent's folder; never / or the home folder) is Development: reads such as ls, rg, git status, test runners, linters, builds, and type checks are Reads; git commit, formatters with --write, package scripts, and mv/cp are Changes; rm, git clean -fd, and git reset --hard are Deletes. Package installs (npm install, pip install, brew install) are Changes in Other apps. git push to the default branch (origin's HEAD, else main/master) is a Change in Production, to any other branch a Change in Development; deleting a remote branch (--delete, :branch) is a Delete in Other apps because teammates share it. Temporary folders (/tmp, $TMPDIR) count as Development; deleting one whole is Irreversible. Outside the project: deleting the home folder, /, system paths, or the whole project is Irreversible; other writes are Changes and deletes are Deletes in Other apps; writes to keys, shell startup files, or an agent's own settings are Deletes, and reads of them are no decision. curl/wget GETs are Reads and anything that sends data is a Change, in Other apps (Development for localhost). docker ps/logs read, run/build/compose up change, rm/prune delete. sudo never runs silently. The project's own code run directly (node scripts/check.mjs, npx tsx scripts/x.ts, bash scripts/ci.sh, ./scripts/ci.sh, make test) is a Change in Development, like a package script; Passport can't read inside it, so it is never allowed and never recorded as opaque. tsx --test and ./node_modules/.bin/<tool> are their tools, bash -lc '…' and sh -c '…' are read as the line inside, and Codex's apply_patch <<'PATCH' is a change to the files the patch names. A checkout an agent keeps for itself (~/.codex/worktrees/…, ~/.cursor/worktrees/…) is the project, not the agent's settings. Symlinks are resolved before deciding, cd is followed through &&, ;, and subshells, and redirects are writes where they land. The server keeps a generated copy of the tables (node scripts/generate-local-commands.mjs) for rules test on the web. Allowed project work and reads are counted on the machine ("Answered for you") rather than recorded one by one; installs, pushes, and anything that asks or blocks are recorded like vendor commands, with the environment kind and never a path.
The environment comes from each vendor's own flags (src/cliHookEnvironment.ts): Railway -e/--environment, Wrangler and Sentry -e/--env, Vercel --prod/--target (a deploy without either is a preview), Supabase --local/--linked/--project-ref (db reset is local unless --linked), Stripe --live (test mode otherwise), git push <remote> <branch>, psql hosts (localhost is Development), fly and Heroku -a/--app, kubectl --context/-n, helm --kube-context/-n, AWS --profile, gcloud --project, plus any flags the server adds. Names follow Passport's rules (prod|production|live|main → Production, stag|qa|uat|preview → Staging, dev|local|test|sandbox|feature|branch → Development), and the workspace's own placement of a name wins. Anything else, or no flag at all (a linked project the hook can't see), counts as Production, and the reason says Environment unknown, treated as Production.
Attended or unattended (src/cliHookMode.ts). With a person at the keyboard, an ask is the agent's own prompt with Passport's reason. With nobody there, the guard asks Passport instead (POST /api/my/cli/decide, inside the hook's budget): the server knows grants already given this session, every rule, and the workspace's environments. An allow from it runs the command (for example, a retry after approval); an ask becomes an approval request on the web and the person's phone, and the agent reads Waiting for approval from Ramish. Run `passport wait <id>` to wait, or continue with other work. If Passport can't answer in time, the local decision stands and the reason adds Passport couldn't be reached, so this computer decided. Signals, strongest first:
| Signal | Means |
| ------------------------------------------------------------------------------------------------- | --------------------------------------- |
| PASSPORT_HOOK_MODE=attended\|unattended | Override |
| Claude Code CLAUDE_CODE_SESSION_ATTENDED (1/0), passed to every hook | Attended / unattended |
| Hook input permission_mode dontAsk or bypassPermissions (Claude Code, Codex) | Unattended: prompts are suppressed |
| Cursor is_background_agent: true | Unattended: background and cloud agents |
| CLAUDE_CODE_ENTRYPOINT sdk-cli (claude -p), sdk-ts, sdk-py, claude-code-github-action | Unattended (older Claude Code builds) |
| CI, GITHUB_ACTIONS, GITLAB_CI, BUILDKITE, CIRCLECI, JENKINS_URL, TF_BUILD | Unattended |
Codex has no field for codex exec; its prompt-free modes count as unattended, where an ask blocks for now with the waiting line.
The approval request carries a command summary with secrets removed on this computer (src/cliHookRedact.ts, using passport scan's key formats plus secret-named flags, variables, JSON fields, URL credentials, and long opaque values; heredoc bodies are dropped). It is the only part of a command line that ever leaves the machine, and only approvers see it.
The guard decides from local data inside HOOK_BUDGET_MS (1.5 s) and prints first; only an unattended ask waits for the server, within the same budget. After answering it never waits on the network: its report, the batches, and, at most every ten minutes, a refresh of a policy cache older than an hour go through the detached passport hook flush. Any failure is silent and fails open. Neither hook sends the raw command line, output, folder path, or environment. Both hooks exchange the profile's refresh credential without rotating it and keep the short-lived access token in $PASSPORT_HOME/.<profile>.access.json (src/accessTokenCache.ts, mode 600, atomic replace), so most runs send one request instead of an exchange and then the request. A cached token is used only with more than a minute left; a 401 on it drops it, exchanges once, and retries once.
An ask becomes one Activity row. In Claude Code, with the audit hook registered, the guard parks its report in $PASSPORT_HOME/hook-pending/ (src/cliHookPending.ts, mode 600, claimed by atomic rename). The audit hook for the same tool-use id sends the run with decision: "ask", ran: true ("asked first, then ran"). An ask nobody claims within 30 minutes (no agent fires a hook for a declined prompt) is sent by the next hook run as ran: false ("asked first, not run"). An allow the server gave is parked the same way, so the run's row says Approved earlier this session. Cursor has no tool-use id to match, so its asks are sent at once as their own "asked first" row. Denies are always sent at once; asks and blocks the server decided are recorded by the server.
- Claude Code:
PreToolUse, plusPostToolUseandPostToolUseFailure(failed or interrupted calls, recorded as ran with a non-zero exit), in~/.claude/settings.json(CLAUDE_CONFIG_DIRhonored). - Codex: the same events in
~/.codex/hooks.json(CODEX_HOMEhonored). Codex sends every shell command astool_name: "Bash"with the model's raw command string intool_input.command(never shell-wrapped), so it is read exactly like Claude Code's; itsapply_patchtool is a file edit, not a shell command, and isn't recorded. Codex supports deny only (an ask fails the hook open, and its allow rewrites commands), so an ask is a deny there that says why (Codex can't ask from a hook, so Passport blocked it. Approve it in Passport or run it yourself.), recorded as a block. With a member profile and Autopilot, it goes to Passport like an unattended ask: an approval request, then the waiting line. Trust the hooks in Codex's/hooks. - Cursor:
beforeShellExecution/afterShellExecutionin~/.cursor/hooks.json. Cursor also imports Claude Code's hooks by default; that imported copy records nothing when Passport's own Cursor entry exists, so rows aren't doubled.
Guard health (src/cliGuardHealth.ts, states and fixes in src/cliGuardState.ts, a checked copy of shared/guardContract.ts). passport hook status and passport status show each agent's guard in the web's words, On, Off, Outdated, Not trusted, or Locked, and each problem's fix: passport hook install --client <agent> --guard --apply, npx passport-bridge@latest init, or Open Codex → /hooks and trust Passport's hooks. At most once a day ($PASSPORT_HOME/guard/.<profile>.heartbeat) the CLI sends POST /api/my/guard-health with {cliVersion, launcherTarget?, clients: [{client, present, mode, guard, plugin, codexTrust?, locked?, disabledEnv?}]}: flags only, never a command, path, or value. It goes from a guard run with at least 300 ms of its budget left (a hook's first one waits a day), from passport exec, and from passport status. Codex trust (src/cliCodexTrust.ts) compares trusted_hash in ~/.codex/config.toml [hooks.state."<hooks.json>:pre_tool_use:<group>:<handler>"] with Codex's own hash of Passport's guard entry: SHA-256 of the sorted-key JSON of {event_name, matcher?, hooks: [{type, command, timeout (600 when unset), async, statusMessage?, additionalContextLimit? (when not 2500)}]}, as in openai/codex hook_hash/version_for_toml, checked against codex-cli 0.146.0's hooks/list. Missing is Not trusted, a different hash is Not trusted (modified), enabled = false is Off. Locked means managed settings hold Passport's guard: /Library/Application Support/ClaudeCode/managed-settings.json (and managed-settings.d/), /etc/claude-code/…, /etc/codex/requirements.toml, /Library/Application Support/Cursor/hooks.json, or /etc/cursor/hooks.json (PASSPORT_MANAGED_ROOT points tests elsewhere).
Coverage (src/cliSeen.ts). The guard counts every command it saw, per agent per day, one character per command in $PASSPORT_HOME/seen/<profile>.<agent>.<day>.log (an app's action, not checked, or neither), and Cursor's imported copy of Claude Code's hook doesn't count twice. Counts go in their own batches, at most every five minutes, as seen: {batch, day, count, governed, opaque} on POST /api/my/cli-activity, never inside a report that can expire. passport status prints Checked: 94% of commands Passport saw this week.
When Passport can't be reached. When the guard needs Passport (an unattended ask, Codex, or a Production action someone else approves) and it doesn't answer within the budget, the environment's offline setting from cli-policy v2 decides: ask, block, or allow (Production asks, the rest run, by default). Allow never covers an irreversible action, a second-person action is always blocked, Codex's ask is a block, and a policy synced more than 7 days ago uses the defaults. The reason ends with Passport couldn't be reached, so this computer decided.
src/cliPolicy.ts caches the member's CLI policy (GET /api/my/cli-policy) at $PASSPORT_HOME/cli-policy/<profile>.json (mode 600). Servers with Autopilot send version 2 (src/cliPolicyV2.ts: presets, floors, CLI rules, irreversible and delete verbs, environment flags, placed environments); older servers send version 1, and then the guard keeps its earlier shape: ask or deny before destructive verbs, never allow, no server call, and reports without the Autopilot fields. passport login and passport status refresh it and passport logout removes it. A 404 or error keeps the previous copy; with no copy the guard asks before every destructive verb. PASSPORT_HOOK_DISABLED=1 turns both hooks off.
The Passport plugin (rsyed94/passport-plugins) runs the same two hooks from the agent's plugin folder, with PASSPORT_HOOK_VIA=plugin. passport hook status shows those agents as via plugin, and hook install, init, and setup --org skip them (Claude Code: already covered by the Passport plugin.) so nothing is recorded twice. The plugin pins the CLI version in src/pluginPin.ts.
Autopilot from the terminal
passport rules test "railway redeploy -e staging"
passport rules test "rm -rf ~/"
passport wait apr_123
passport sessions
passport sync # or: passport sync --remove
passport env # or: passport env set railway qa staging
passport actions add scripts/deploy.py --environment production --class change
passport actions sync # propose this repository's .passport/actions.tomlenvlists where commands in this folder land, read the way each vendor's own CLI reads it (src/envResolve.ts; order: flag, the folder's link, the vendor's default, the workspace's saved mapping, the whole-word naming rule, else Production). Link readers:src/envLinksHosting.ts(Railway~/.railway/config.json, Vercel.vercel/project.json/repo.json, Supabasesupabase/.temp/project-ref, Wrangler config, Netlify.netlify/state.json, Firebase.firebaserc+ configstore, ConvexCONVEX_DEPLOYMENT, Neon.neon),src/envLinksCloud.ts(Fly, Heroku git remotes, kubeconfig, AWS profile, gcloud configuration, Terraform workspace, Stripe key mode), andsrc/envDatabase.ts(database URLs: onlyDATABASE_URL,DIRECT_URL,POSTGRES_URL*,DATABASE_URL_UNPOOLED, reduced to a host-derived key; the URL, user, and password never leave the function). Files are read fresh for every command, size-capped, and a link file symlinked out of the project is ignored (src/envLinkFs.ts). For a placed id its mapping decides, and a different name in the link file is treated as a rename (sent as the new label, which the server stores). A name in a link file never loosens anything: an unplaced id is named only by a label the server confirmed through the connected app (resources[].confirmed), which the naming rule then reads (source: "name", shown asStaging (from its name)); without one, or when it says nothing, it is assumed Production. The table keeps its columns when every row fits the terminal and stacks each row otherwise. Findings to a server that sendsresourcesin cli-policy v2 carry{kind, source, resourceKey?, label?, platform?, flag?, via}.env setcallsPUT /api/my/environments/:platform/:key; members may only say Production.rules testclassifies the command here and sends only its app and verb (and the environment kind) toPOST /api/my/rules/test, never the command line. An everyday command goes as a finding ({finding: {vendor, verb, risk, environment}}), because only this computer can place its paths. It prints the decision, its cause, the environment, and the class. Without a server answer it decides from this computer's copy and says how old it is.--environmentpicks one when the flags don't say.waitlong-pollsGET /api/my/approvals/<id>/wait. Approved:Approved by Ramish. Run the command again.(exit 0). Declined: the note for the agent,Declined by Ramish: <note>(exit 5, causeapproval_denied). Expired:Expired. The agent can ask again.(exit 5, causeapproval_expired). Still waiting at--timeout(default 100 seconds, under an agent's 2-minute command limit): exit 4 with the same command asnext.sessionslists recent sessions (GET /api/my/agent-sessions): status, agent, folder, start,42 ran · 2 asked · 1 blocked, environments touched, andWaiting for youwhen approvals are pending.syncwrites the allows Passport is sure of into each agent's own permission settings, so those prompts disappear offline and in Codex (src/nativeSync.ts). Only reads, test runners, builds and type checks, linters that only check,git status,git add, andgit commit, checked against the person's rules. Formatters, fixing linters,mv/cp, and package scripts stay with the hook, which sees what they touch; deletes, installs, pushes, and Production never sync. Claude Code:Bash(npm test *)inpermissions.allowof~/.claude/settings.json. Codex:prefix_rule(pattern = ["npm", "test"], decision = "allow")in~/.codex/rules/passport.rules, a file Passport owns. Cursor CLI:Shell(npm:test*)inpermissions.allowof~/.cursor/cli-config.json. Cursor:npm testinterminalAllowlistof~/.cursor/permissions.json, only when that list already exists there (defining it replaces Cursor's in-app list). Entries Passport added are listed in$PASSPORT_HOME/native-sync.json; an entry you had before is never claimed or removed, and a file Passport can't parse exactly (JSONC) is left alone. Sync runs oninit,setup --org, every rules refresh, andpassport sync;hook uninstall,sync --remove, and signing out of the last profile take the entries out.statusadds each agent's guard (Guard: Claude Code On · Codex Not trusted), the coverage line, and each fix, and sends the daily guard report when due. It also adds today's speed line,Passport added 0.3 s and asked you twice today. 412 actions ran on their own.(across your computers when the server keeps speed numbers, else(this computer)), andAnswered for you: N prompts today(allows the hook gave in Claude Code and Cursor, and Codex commands Passport synced, counted only when the agent would have prompted) and which agents' allowlists are synced. Under the presets, one line per exception that lets something run your presets would ask about, in the web's words:! 1 exception: deletes run in Production without asking, even without a backup.Counts are appended locally ($PASSPORT_HOME/answered/) and sent in batches at most every five minutes asanswered: {batch, count}onPOST /api/my/cli-activity.actionstells Passport what a project script does (src/cliActions.ts):actions add <script> [args…] --environment <kind> --class <class>sends the path in the repository (ormake,npm,pnpm,yarnwith the target), an argument glob, the repository's remote fingerprint and name, and the SHA-256 of what it runs, never its contents, toPOST /api/my/project-actions.--id <op_…>resolves a Session row's Tell Passport what this does from this computer's scan cache.actions syncproposes each entry of.passport/actions.toml. A change, delete, or irreversible in Production applies at once; anything looser waits for an admin. The guard then decides a matching run as vendorprojectwith that class and environment; it never makes a line allowed on its own, and a changed script stops matching (projectChanged) until it's set again..passport/actions.tomlentries apply locally only when they ask or block.rules test,sessions,actions, andsyncneed a member profile.waitalso works for a cloud agent's key, on the approval its ownpassport execopened.
Keys and passport exec
passport keys move # confirm each key [y/n], store it in Passport, sign that CLI out here
passport keys # what Passport holds, and what's still on this computer
passport keys add railway # store a key you paste (stdin, never echoed)
passport exec -- railway up -e staging- After a move,
keys moveremembers where each removed key was in$PASSPORT_HOME/keys-moved.json(vendor,~/…location, file, field names, size, modified time; never a value or hash). With the daily guard report, files that changed are read again; a key that's back is reported asreappeared: [vendor]onPOST /api/my/keys/report, so the key reads "Back on " in Apps with the fixpassport keys move. Kept keys are never listed. The report's computer is the CLI's own sign-in (its Device); the name it sends stays for older servers. keys movefinds CLI sign-ins (Railway, GitHub CLI, Vercel, Wrangler API tokens, Supabase, Stripe test keys, the AWSdefaultprofile) and tokens in Claude Code's settingsenv. For each it asks[y/n]with no default (an empty answer asks again;--yesmoves them all), stores the key in Passport (POST /api/my/keys), then signs that CLI out by removing only the credential: the token fields of~/.railway/config.json(project links stay),tokenin Vercel'sauth.json(not revoked; the current team stays), Wrangler's sign-in keys indefault.toml, the Supabaseaccess-tokenfile and its keychain token (saved database passwords stay), the Stripe profile's API keys and its live key in the keychain (account, color, and other profiles stay), the AWS profile's key lines, or the one settings entry. Files are written atomically with their mode kept. Vendor sign-outs don't run because they do more (railway logoutunlinks every project,vercel logoutrevokes the token Passport just stored); only the GitHub CLI'sgh auth logout --hostname github.comruns, adding--user <active>when several accounts are signed in, so other hosts and accounts stay. Keychain items are deleted by exact service and account (security delete-generic-passwordon macOS,secret-tool clearon Linux); where that isn't possible the row says which keychain copy may stay. Wrangler OAuth tokens are removed locally, not revoked. A sign-out that fails leaves the key in Passport and says what's left on disk..envfiles are never touched: "Used by your app, so Passport leaves it." Browser-session sign-ins (Railway, Vercel, Wrangler OAuth) can't move; the line says how to make a token instead. A different key than the one already in Passport is replaced only when the person answers yes toReplace the <CLI> key in Passport with the one on this computer? [y/n]; the CLI then names the key it replaces (replace: <keyId>onPOST /api/my/keys). The server alerts the owner and admins with an Undo, records it in the audit trail, and keeps the old value encrypted for seven days (POST /api/my/keys/:id/restorefrom the browser, or the alert). Without a person (--yes, no terminal), both keys stay. The scan report sent afterwards carries provider counts only.exec -- <cli> …runs railway, gh, vercel, wrangler, supabase, stripe, aws, neonctl, or flyctl from PATH. It classifies the command here, asksPOST /api/my/exec/authorize, and on allow sets only that vendor's variables (GH_TOKEN,RAILWAY_API_TOKEN,VERCEL_TOKEN,CLOUDFLARE_API_TOKEN,SUPABASE_ACCESS_TOKEN,STRIPE_API_KEY,AWS_*,NEON_API_KEY,FLY_API_TOKEN) in the child's environment, removing Passport's own and any stray vendor tokens. Output and exit code pass through. Afterwards it reports the exit code and duration (POST /api/my/exec/result, best effort, two seconds at most) so the session row reads Ran or Failed. It refuses paths, shells,env, and subcommands that would print the key, sign in again, mint longer-lived tokens, or run another program with the key (railway run,gh auth, gh extensions,aws configure,vercel dev,fly tokens).- Asks wait for your approval in Passport (web or push) when you're at the keyboard or with
--wait <sec>; otherwise exit 4 with the approval id: runpassport wait <id>, then the command again. An approval is used once. Blocked or declined: exit 5. No key, or it stopped working: exit 3 ("Your Railway key in Passport stopped working. Update it in Apps."). Offline: exit 1, nothing runs, and exec never uses a key on this computer instead. A team that requires the guard refuses exec for an agent whose computer hasn't reported its guard On in 48 hours: exit 5 withBlocked by Passport: guard required…and the fix. - In Claude Code, the guard rewrites a plain vendor command whose key is in Passport to
passport exec(PreToolUseupdatedInputwith allow; a block stays a deny). When the rules ask, or someone is at the keyboard, the rewrite adds--wait 120, so the agent holds while the person approves: "Approve it on your phone or in Passport." Codex and Cursor get no rewrite: usepassport execthere. - Cloud agents (Agents → Cloud agents) run exec and MCP with
PASSPORT_AGENT_KEYandPASSPORT_ENDPOINT: the owner's rules decide, the owner's keys are used, and approvals go to the owner. - Backups: for
gh issue close,gh pr close,gh api -X DELETEof a branch,vercel env rm,vercel rollback, andvercel promote, exec also sends the ids Passport needs to take a backup first (repo and number or branch; the linked Vercel project and variable name from.vercel/project.json), never a value. After the run it printsBackup taken. Undo within 7 days: passport undo <action-id>, orNo backup: <reason>for a delete it couldn't back up.
Undo
passport undo act_123
passport undo --last --yes- Shows what comes back and what won't, how old the backup is, and how long undo stays available, then asks
Undo it? [y/N](and which option, when there are several). Without a terminal it prints the preview andpassport undo <id> --yes;--option <id>picks an option. - Passport decides an undo from the CLI like a change in that environment. Asks first: exit 4 with the approval id; run
passport wait <id>, then the undo again. Blocked or declined: exit 5. Done: exit 0, with each step (✓/✗). Partly done or failed: exit 1. Expired, already undone, or otherwise unavailable: exit 5 with the reason. No backup: exit 6. Needs a member profile.
Persistent sessions
printf '%s\n' \
'{"id":"find","argv":["search","open issues"]}' \
'{"id":"call","argv":["run","github_search_issues","{\"query\":\"SSO\"}"]}' \
| passport session --profile cli@ws_example- One
{"id"?:string|number,"argv":string[]}request per line, run in order; one JSON response per line withok,exitCode, andresultor error fields.idis echoed. - A failed request does not close the session. EOF, SIGINT, or SIGTERM closes it cleanly.
- Limits: 1 MiB per line, 128 arguments. Set
--profile,--caller, and--timeoutonpassport session, not per request. - Allowed commands:
status,search,describe,run,connect,resources,prompts,discover. - Governance is unchanged; access tokens keep normal expiry and refresh.
Shell completion
passport completion bash > ~/.local/share/bash-completion/completions/passport
passport completion zsh > ~/.zfunc/_passport # ~/.zfunc must be in fpath before compinit
passport completion fish > ~/.config/fish/completions/passport.fishProfiles
Credentials live in ~/.passport/credentials.json (mode 0600, atomic updates); PASSPORT_HOME changes the directory. The store is shared with passport-bridge, and refreshes are coordinated across processes. The hooks cache each profile's current access token next to it in .<profile>.access.json (mode 0600): the token, its expiry, the endpoint, and a sha256 fingerprint of the refresh credential (never the credential itself), so a re-login, rotation, or endpoint change can't reuse it. Logging out, signing in again, or rotating a profile deletes its cached token.
Resolution order:
--profile <name>PASSPORT_PROFILE- Active profile (
passport use <name>) - Exactly one CLI-owned profile, which becomes active
- The sole stored profile
Otherwise Passport refuses to guess: run passport profiles, then passport use <name>. Names are 1-80 letters, numbers, ., _, @, or -, starting with a letter or number.
A distinct agent identity for policy, selection, devices, and activity, while keeping the CLI credential surface:
passport login --workspace ws_example --agent claude-code --profile claude-code@ws_exampleCaller attribution
--caller <id> (1-24 lowercase letters, numbers, hyphens) adds activity detail for the agent behind a call. Without it, the CLI detects Claude Code, Codex, OpenCode, Cursor, or Gemini CLI from environment markers. Attribution is self-reported, not attested, and is accepted only from CLI-surface credentials or CLI agent-key requests.
Headless and CI
export PASSPORT_ENDPOINT=https://passport.example.com/mcp
export PASSPORT_AGENT_KEY="$CI_PASSPORT_AGENT_KEY"
passport ready --json
passport search "deployment status" --jsonPASSPORT_ENDPOINT is Passport's one URL (https://<host>/mcp, where the agent key's own prefix says which workspace) or a workspace's own (https://<host>/w/<workspace>/mcp). Set both variables together. Environment auth overrides stored profiles, persists nothing, and cannot be combined with --profile or login/logout. Agent keys skip member approval prompts; use a member profile for work that must pause for a human.
Managed access
Workload credentials that sign private_key_jwt assertions. Requires PostgreSQL and Enterprise workspace controls, with connections and grants set up in Agents & access. Creating new delegations is retired; existing delegation IDs still work.
passport access keygen --algorithm ES256 # or RS256; never overwrites; private PEM is owner-onlyRegister only the public JWK in Agents & access, then configure the workload:
export PASSPORT_ENDPOINT=https://passport.example.com
export PASSPORT_CLIENT_ID=ws_example~awc_example
export PASSPORT_PRIVATE_KEY_FILE=/run/secrets/passport-access.pem
export PASSPORT_WORKSPACE_ID=ws_example # optional; defaults to the client ID prefix
export PASSPORT_CALLER_ID=build-job-1 # or --caller; required for invoke
passport access actions --json
passport access run --input run.json --json
passport access invoke --input invocation.json --json
passport access status <invocation-id> --json
passport access cancel <run-id> --jsonrun.json: non-emptycapabilitiescopied fromactions; optionaldelegationId,expiresInSeconds(60 to 86,400),callLimit(1 to 10,000).invocation.json:runId, a stable uniqueinvocationId,appId,tool,resource,connectionId, andargs.- Approval pending: after a human approves, invoke again with the same file and caller. Timeout or unknown result: check
access statusbefore any new write. Provider writes are never retried automatically. - Delegated work:
passport access actions --delegation-id <id> --json, then include the samedelegationIdinrun.json. The agent and the delegated person both need matching allow grants. - Tokens are never printed.
SDK, from the package root or passport-bridge/access:
import { accessClientFromEnv } from "passport-bridge/access";
const client = accessClientFromEnv(process.env, { callerId: "build-job-1" });
const actions = await client.actions(); // or client.actions(delegationId)AccessGatewayClient exposes actions, createRun, invoke, status, and cancel.
Output contract
With --json, stdout is exactly one JSON value; hints and warnings go to stderr.
| Command | JSON value |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| login | {profile,endpoint,client,surface,active,workspaceId,workspaceName?,next} (workspaceName after browser sign-in) |
| profiles | {profiles:[{name,active,client?}],active?} |
| use | {profile,active:true} |
| logout | {profile,removed,revoked} |
| status | {workspace,member,endpoint,auth,profile?,client,mcps,autopilot?:{presets,floors,floorReason?,exceptions?,cache:{version,fetchedAt,ageMs}},next?} |
| wait | {approvalId,status:"approved",scope?,message}; declined, expired, and still-waiting are errors with approvalId |
| rules | {command,decision,cause?,explanation?,environment?,environmentAssumed?,actionClass?,source} |
| sessions | {sessions:[{id,client,folder,status,startedAt,lastActivityAt,counts,environments,waiting}],nextCursor} |
| undo | {actionId,status,message,steps:[{label,ok,detail?}],option,undoActionId?}; without --yes off a terminal, {actionId,status:"preview",preview,next}; pending, blocked, and declined are errors |
| doctor | {version,ok,auth,profile?,profileCount,endpointOrigin?,server?,checks}, also on failure |
| ready | {ready,version,ok,auth,profile?,profileCount,endpointOrigin?,server?,checks}
