@pagespace/cli
v1.11.0
Published
pagespace — the official CLI for PageSpace: browser-based login, drive/page/task management, and a `pagespace mcp` stdio server for coding agents.
Readme
@pagespace/cli
pagespace is the command-line client for PageSpace — work with your
drives, pages, tasks, search, and AI agents from the terminal. It also ships pagespace mcp, a
stdio MCP server that gives coding agents (Claude Code,
Cursor, …) the same tool surface.
Every CLI verb, MCP tool, and @pagespace/sdk
method is generated from one operation registry, so the three surfaces never drift apart.
Install
npm install -g @pagespace/cli
# or run without installing:
npx -y -p @pagespace/cli pagespace whoamiThis installs two commands: pagespace (the CLI) and pagespace-mcp (an alias for
pagespace mcp, for zero-install MCP configs — see pagespace mcp).
Bare
npx @pagespace/clican't work: the package publishes two bins and neither is namedcli, sonpxhas no default to pick. Always name the bin:npx -y -p @pagespace/cli <bin>.
Quickstart
# 1. Log in — opens your browser for an OAuth 2.1 + PKCE flow.
pagespace login
# 2. Mint a drive-scoped access key. A guided wizard picks the drive(s) and role,
# opens the browser once more for consent, and stores the key under a name you choose.
pagespace keys
# 3. Activate it for this machine — one quick browser approval.
pagespace keys use <name>
# 4. Commands just work now.
pagespace drives list
pagespace search text "roadmap" --all-drivesThe model behind those four commands:
- Your login can't touch content.
pagespace logingrants key management only (manage_keysscope). Content access always comes from a key — a drive-scopedmcp_credential you mint in step 2, stored in your OS keychain under the name you picked. - Scope changes happen in a browser; use doesn't. Minting a key, editing its drives, and
activating it on a machine each require a human approving a consent screen. Once a key is
active,
pagespacecontent commands run by this OS user — by you, a script, or a coding agent driving the CLI in bash — use it by default, bounded by the drives and role you approved. Explicit credentials (--key/--token/env) always override the active key;pagespace keys use --offdeactivates it. pagespace mcpis deliberately excluded from the active key — MCP configs name their credential explicitly so they stay portable and self-describing (see below).- A key can describe itself, but cannot manage keys.
pagespace keys describereports the credential in use — drives, role, and the permissions it actually resolves to — and works under a key. Because it reports on a content credential rather than managing keys, it is the onekeysverb that follows the content-command rule above: name the credential (--key=<name>,--token, the env vars, or an active key).keys list/revoke/useand the wizard manage the whole set of keys you hold, so they need your login instead, and refuse a key outright rather than failing halfway through.
No browser on this machine (CI, container, remote box)? pagespace login --device prints a
short code and URL you approve from any browser. Keys are different — their consent redirect
lands on 127.0.0.1 of the machine running the command, so mint (and activate) where you have
a browser, and hand a headless machine a portable token instead: keys create … --show-token
prints it exactly once (see Need the raw token?).
Credentials
pagespace login is you. Keys are capabilities. Minting, re-scoping, or activating a key
always passes through a browser consent screen; there is no silent, agent-runnable way to
create a credential or widen what's ambiently available. That consent screen is a barrier
against silent privilege escalation, not against a same-OS-user process that can already read
the credential store — see Agent access & isolation for the exact
trust boundary.
| Command | What it does |
|---|---|
| pagespace login [--host <url>] [--yes] | Browser (loopback + PKCE) login. Stores your personal login credential, scoped to manage_keys offline_access — zero content access. --yes overwrites an existing stored credential. |
| pagespace login --device | Device-authorization login for machines without a browser. Same scope. |
| pagespace logout [--host <url>] [--key <name>] [--all] [--force] | Revokes and removes a stored credential — one host, or every host with --all. --force removes the local copy even if server-side revocation fails. |
| pagespace whoami [--json] | Shows the identity and scope of the current credential, plus this machine's active key. |
| pagespace keys | Interactive wizard: create, list, edit (re-scope in place, same secret), set active, and revoke keys. Needs a real terminal; in scripts use the subcommands below. |
| pagespace keys create --drive <id> [--role member\|admin\|<customRoleId>] [--drive … --role …] [--name <name>] [--show-token] [--yes] | Mints a key scoped to the given drive(s) via browser consent, then stores it under --name (defaults to the drive id; required for multiple drives; default is reserved for your login). --yes overwrites an existing key of the same name. |
| pagespace keys use <name> / pagespace keys use --off | Makes a stored key this machine's active key (browser approval), or deactivates it locally. See above. |
| pagespace keys list [--json] | Lists your keys (prefix only — never the secret), with the role granted on each drive. Needs your login — a key cannot list keys; see keys describe. |
| pagespace keys describe [--page <pageId>] [--json] | What the credential this machine would use actually is: its drives, the role granted in each, and the effective permissions that role resolves to (view/edit/share/delete). Drive-level permissions cover the drive itself (creating a top-level page, sharing or deleting it); a page inside can be narrower, so --page resolves that page too. The one keys verb a key can run about itself — and, unlike its siblings, it reports on a content credential, so it needs one named: --key=<name>, --token, the env vars, or an active key. A bare pagespace keys describe with only a personal login is refused, like any other content command. |
| pagespace keys revoke <tokenId> [--yes] | Revokes a key server-side. Irreversible. |
--role binds to the --drive immediately before it, not to every --drive on the command
line: --drive a --drive b --role admin grants admin only on b; a gets no fixed role at
all — it's scoped to whatever role you personally hold on drive a at request time, which
changes if your own membership does. Give every drive its own --role when you want a fixed
grant: --drive a --role admin --drive b --role admin.
A plain pagespace login is enough to drive these — with one exception. keys create, list,
revoke, use and the wizard all manage keys, so they need your login and nothing else
(keys create brings its own browser consent). keys describe is the exception: it reports on
a content credential rather than managing keys, so it needs one named — --key=<name>,
--token, the env vars, or an active key — exactly like the content commands below. A bare
pagespace keys describe with only a login is refused.
Everything else — the content commands — needs a credential, resolved as described next.
How commands find a credential
Highest precedence first:
--token / --key flags > PAGESPACE_TOKEN / PAGESPACE_KEY env > the active key (pagespace keys use) > loud refusal--host / PAGESPACE_API_URL select the host the same way (default https://pagespace.ai).
Explicit always wins — an agent handed --key ci can never be silently retargeted by the
machine's active key. With nothing explicit and no active key, content commands refuse with
instructions rather than falling back to your login credential (which has no content access
anyway). pagespace mcp additionally never uses the active key.
Need the raw token? (CI, another machine)
By default a mint never displays the secret — it goes straight into your keychain. When you
need a portable mcp_… token for an .env file, CI secret, or a different machine:
pagespace keys create … --show-tokenprintsPAGESPACE_TOKEN=mcp_…once as the only stdout line (pipe-friendly:… --show-token | pbcopy). It is never shown again.- The wizard offers the same show-once choice after a mint.
- Or mint from Settings → MCP in the web app.
Anyone holding a raw token gets that key's access — prefer named keys whenever the consumer runs on the machine that minted them.
Command reference
Every command is pagespace <resource> <verb> [args] [flags]. pagespace help prints this
list in the terminal; pagespace --version prints the CLI and SDK versions. Global flags,
accepted everywhere: --json (machine-readable output on stdout, nothing else), --host <url>,
--token <token>, --key <name>, --timeout <seconds> (how long to wait for the one request the
command makes), and --yes (skip confirmations).
drives list [--all] # --all includes trashed drives
create <name>
rename <driveId> <name>
update-context <driveId> <drivePrompt> # sets the drive's AI context prompt (max 10000 chars)
set-home-page <driveId> <pageId|--clear>
trash <driveId> [--yes] # asks you to type the drive name unless --yes
restore <driveId>
roles list <driveId>
get <driveId> <roleId>
create <driveId> <name> [--description <text>] [--color <hex>] [--is-default true|false]
[--drive-wide-view true|false --drive-wide-edit true|false --drive-wide-share true|false]
update <driveId> <roleId> [--name <text>] [--description <text>|--clear-description] [--color <hex>|--clear-color]
[--is-default true|false] [--drive-wide-view/--drive-wide-edit/--drive-wide-share true|false | --clear-drive-wide]
delete <driveId> <roleId> [--yes]
set-page-permissions <driveId> <roleId> <pageId> --view true|false --edit true|false --share true|false
set-drive-wide-permissions <driveId> <roleId> --view true|false --edit true|false --share true|false
remove-page-permissions <driveId> <roleId> <pageId> [--yes]
pages list --drive <driveId> [parentId]
tree --drive <driveId> [parentId]
read <pageId> [--start N] [--end M] [--raw]
read-details <pageId>
create <title> <type> [parentId] --drive <driveId>
rename <pageId> <title>
move <pageId> <newParentId|root> <newPosition>
replace-lines <pageId> --start N [--end M] [--file <path>]
export <pageId> --format md|csv --out <path|-> [--force]
trash <pageId> [--all] [--yes]
restore <pageId>
files upload <path> --drive <driveId> [--parent <pageId>] [--title <title>] [--mime <type>]
sheets describe <pageId> # tabs and sizes, reads no rows
query <pageId> [--where <json>] [--select A,B] [--order-by A:desc] [--limit N] [--offset N] [--tab N]
rows <pageId> [--from-row N] [--limit N] [--tab N]
append <pageId> [--json-input <json>] [--tab N]
update-cells <pageId> [--json-input <json>] [--tab N]
delete-rows <pageId> --from-row N --count N [--tab N] [--yes]
edit-cells <pageId> [--json-input <json>]
formatting <pageId> [--ranges A1:F40,H2:H9] [--tab N] # regions, rules, freezes, formats
format <pageId> [--json-input <json>] [--tab N] # apply formatting ops, in order
trash list --drive <driveId>
tasks list <taskListPageId>
create <pageId> --title <title> [--priority low|medium|high] [--status <slug>] [--due <date>] [--assignee <userId>]
update <pageId> <taskId> [--title <title>] [--status <slug>] [--priority low|medium|high] [--due <date>]
delete <pageId> <taskId> [--yes]
reorder <pageId> <taskId> <position>
statuses <taskListPageId>
create-status <pageId> --name <name> --color <color> --group todo|in_progress|done [--position N]
assigned # tasks assigned to you, across drives
search text <query> [--drive <driveId>|--all-drives] [--max-results <n>]
regex <pattern> --drive <driveId> [--in content|title|both] [--max-results <n>]
glob <pattern> --drive <driveId> [--max-results <n>]
agents list --drive <driveId>|--all-drives
ask <agentPageId> <message> [--conversation-id <id>] [--context <text>]
config <agentPageId> --set <key>=<value> [--set <key>=<value> …]
conversations list <agentPageId> # an agent's conversations, newest first
read <agentPageId> <conversationId> # the messages in one conversation
models list
activity <driveId>
channels send <channelId> <message>
workspaces list [--drive <driveId>] # your agent workspaces; each has one sandbox
exec <workspaceId> [--cwd <dir>] [--timeout-ms <ms>] -- <command…> # exits with the command's own status
# one argument runs as a shell command line (-- 'ls | wc -l');
# several are separate words, quoting preserved (-- sh -c 'echo hi; exit 3')
keys (no args: guided wizard) · create · use · list · describe · revoke
env enroll <enrollmentId> <code> # bind THIS machine to a local environment (one-time code)
token <enrollmentId> # prove this machine holds its key; prints a short-lived bridge token
mcp serve the MCP stdio server # see belowOne exception to the global flags: keys create ignores --json — its stdout is either
ordinary status text or, with --show-token, exactly the one PAGESPACE_TOKEN=… line.
pagespace env — this machine as a local environment
What you are agreeing to
pagespace env connect lets PageSpace run commands and read and write files on this computer,
under your own user account. Nothing runs unless a policy file you own allows it, and in ask
mode you see the exact command before it runs. Two things follow from approving one, and the
second surprises people:
- It runs with your privileges, exactly as if you had typed it into this terminal yourself. It can read any file you can read, change any file you can change, reach anything on your network that you can reach, and use any credential sitting in your home directory. There is no sandbox around it.
- You see the first command of each kind, and approving it covers every later command of
that kind for the rest of that session, unseen. The approval is remembered per user, session
and operation — not per command — so after you approve one
exec, the nextexecfrom that same user and session runs with no prompt at all, whatever it is.
The roots in your policy file confine the paths an agent can name — the working directory it
asks for, and the files it asks to read or write. They do not confine what a program does once
it has started. A command approved with a working directory inside a root can still read
/etc/passwd, your SSH keys, or anything else your account can reach; it just cannot ask the
daemon to open them. Actually confining a running process needs operating-system sandboxing,
which this daemon does not do.
So the question at the prompt is not "may this touch that folder". It is "may this, and everything like it for the rest of this session, run as me". Answer it the way you would answer a stranger asking for your shell for the afternoon. Two practical consequences:
- Run the daemon as an ordinary user, never as root, and prefer a machine — or a separate account — that does not hold secrets you would not hand to whoever is driving the agent.
principalsis the list of PageSpace users allowed to drive this machine. Keep it short, and treat adding a name to it as the same decision as giving that person your shell.
What the daemon does guarantee is narrower than "safe", and worth knowing exactly:
- It never listens on a port — it dials out to PageSpace and nothing can dial in.
- It denies by default. No policy file, an unreadable one, or one anybody else can write means every request is refused.
- Every request carries a server-signed grant bound to that exact command or path list: it is single-use, expires within a minute, and cannot be replayed or edited in flight.
- Your policy is checked after the grant and independently of it. A request PageSpace considers allowed is still refused here if your policy does not allow it.
- Every decision — allowed, denied, declined — is appended to
~/.pagespace/env-audit.jsonl. - Revoking the environment in PageSpace deletes this machine's key and stops the daemon;
Ctrl-C and
pagespace env disconnectstop it too, and kill anything it started.
One consequence of the two facts above, worth seeing once before you pick a mode: an agent that
reads a file on this machine can be steered by what that file says — a README, a dependency's
source, a downloaded document can all contain text aimed at the agent rather than at you. If the
agent then runs a command, and you have already approved an exec in that session, that command
runs without a prompt. This is inherent to agents reading content nobody vetted, not a defect in
this daemon; the defences that apply here are the ordinary ones — keep principals short, keep
ops narrow, stop the daemon when you are not using it, and read
~/.pagespace/env-audit.jsonl when you want to know what actually ran.
The three modes, in practice
deny— nothing runs, ever. The daemon still connects, so the Environment shows as connected.ask— the safe default to start with. An operation listed inopsruns without asking; anything else stops and prompts you in this terminal, showing the exact command, working directory, paths, environment and limits. As above, approving covers further requests for that same operation from the same user and session while the daemon runs — those later commands are never shown to you — but not forever, and not for other people. Declining refuses that request and asks again next time.askneeds a terminal: a headless machine cannot use it.allowlist— only the operations inopsrun; everything else is denied with no prompt.
allowlist allowlists operations, not executables. ops holds exec, fs_read and
fs_write — so putting exec in it under allowlist mode means any command may run
unprompted, not some approved list of programs. There is deliberately no executable allowlist:
one was considered for this release and not adopted, because a list of program names is easy to
walk around (sh -c …, an interpreter, a script inside a root) and would suggest a guarantee the
daemon cannot keep. If you want per-command review, use ask and leave exec out of ops.
A drive owner or admin creates a local Environment from the ordinary "New environment" step (choose This computer and name the machine) and is shown a one-time enrollment code (valid ten minutes, single use) beside these exact commands. Losing the code is not fatal: until a machine has enrolled, the Environment's menu in the sidebar offers Show a new code, which replaces the old one. Once a machine has enrolled, no new code can ever be issued for that Environment. On the machine:
pagespace env enroll <enrollmentId> <code> [--host <url>]
pagespace env token <enrollmentId> [--host <url>]
pagespace env connect <enrollmentId> [--host <url>]
pagespace env disconnect <enrollmentId>
pagespace env policy [--json]env enroll generates an Ed25519 keypair on this machine, sends the server the public half
with the code, and stores the private half — plus the server signing key it pinned in return — in
this machine's credential store under the profile env:<enrollmentId>. The private key is never
printed (not even with --json) and never leaves the machine. A refused enrollment discards the
key. env token proves possession of that key (the server issues a nonce, the machine signs it)
and prints a short-lived bridge token — the round trip env connect performs on every
reconnect. Neither command needs a login: the code, then the key, are the machine's credentials,
and the machine credential is never used to authenticate ordinary commands (logout and
keys use refuse it). The deployment must set LOCAL_ENVS_ENABLED=true; otherwise these
endpoints do not exist.
env connect — the bridge daemon
env connect is the daemon that makes the Environment usable. It opens an outbound socket to
PageSpace (it never listens on a port), earns a fresh token from the machine key on every
(re)connect, sends a machine-signed hello, and then serves requests. Every request carries a
grant signed by the server key pinned at enrollment; the daemon verifies the signature, expiry,
env, single-use nonce and that the grant is bound to exactly this request before consulting
your policy, and runs nothing unless the policy allows it. Results are signed with the machine key
so the server can verify them. Everything is appended to ~/.pagespace/env-audit.jsonl
(PAGESPACE_ENV_AUDIT_LOG to move it) as one JSON line per decision with the grantId the server
also audits. Ctrl-C, env disconnect (which signals the running daemon's pid from
~/.pagespace/env-connect.<enrollmentId>.pid), or a server-signed revoke stops it; a revoke also
deletes the machine key. After a server restart it reconnects with exponential backoff.
In this release the daemon serves exec, fs_read and fs_write; PTY sessions are advertised as
unsupported.
Platform: env connect runs on macOS and Linux only. It relies on POSIX process groups to
kill a timed-out command and on O_NOFOLLOW to open files safely, neither of which Windows
provides, so on Windows it refuses to start with a clear message. env enroll, env token,
env disconnect and env policy work everywhere.
Transport: the host must be https:// (the daemon sends this machine's proof of possession
and a short-lived bearer token, and the socket is wss://). Plain http:// is accepted only for
the loopback hosts used in local development (localhost, 127.0.0.1, ::1), matching the CLI's
OAuth loopback policy. Token redemption refuses to follow redirects.
Stopping it: env connect writes its process identity (pid, start time, argv0) to
~/.pagespace/env-connect.<enrollmentId>.pid and refreshes it periodically. env disconnect
validates that record — it must be ours, recent, and the process must still be alive — before
sending SIGTERM, and removes a stale file rather than risk signalling a reused pid.
The policy file
The daemon reads ~/.pagespace/env-policy.json (or the path in PAGESPACE_ENV_POLICY). You
own this file; PageSpace never sees or edits it. It must be owned by the user running env connect
and must not be writable by group or world (chmod 600) — otherwise it is ignored. No policy file,
or one that is unreadable, invalid, wrongly owned or writable by others, means every request is
denied (no_policy); the daemon still connects so you can see the Environment and fix the file.
pagespace env policy prints what is in force, or why the file is being ignored.
{
"mode": "ask",
"principals": ["usr_01hzx…"],
"ops": ["fs_read"],
"roots": ["/Users/me/code/my-project"],
"envAllowlist": ["CI", "NODE_ENV"],
"maxBytes": 1048576,
"maxTimeoutMs": 120000
}mode— see "The three modes, in practice" above. In short:askprompts for anything not inopsand needs a TTY;allowlistruns only what is inopsand never prompts;denyruns nothing.principals— PageSpace user ids allowed to drive this machine. A request from anyone else is deniedprincipal_not_allowed, whatever the server thinks. Each of these users can run commands as you on this machine, subject tomodeandops.ops— any ofexec,fs_read,fs_write(pty_openis reserved for a later release). These are operation kinds, not programs:execcovers every command, and there is no per-executable allowlist.roots— absolute directories every working directory and every file path must resolve inside (symlinks are resolved;..is refused). Roots confine the paths an agent can name. They do not confine a command once it is running: an approvedexecwith its cwd inside a root still runs as you and can read anything you can read. See "What you are agreeing to" above.envAllowlist— environment variable names the server may set for a command. Loader and interpreter hooks (LD_*,DYLD_*,PATH,NODE_OPTIONS, …) are refused even if listed.maxBytes/maxTimeoutMs— caps on captured output and wall-clock per command; a request that asks for more is clamped, and a command that outlives its timeout has its whole process group killed. Both are optional (defaults 1 MiB and 120 s).
pagespace mcp
Runs a stdio MCP server whose tools are generated from the same operation registry as the CLI verbs — identical capabilities, zero drift. Auth resolves like every other command except that the active key deliberately does not apply: an MCP config must name its credential explicitly, so the config stays portable and self-describing, and it never silently picks up whatever key you last activated.
Zero-install (the usual way to wire an MCP client — e.g. Claude Code's .mcp.json):
{
"mcpServers": {
"pagespace": {
"command": "npx",
"args": ["-y", "-p", "@pagespace/cli", "pagespace-mcp"],
"env": { "PAGESPACE_KEY": "agent" }
}
}
}After a global install, "command": "pagespace", "args": ["mcp"] does the same thing.
Which credential goes in env:
- MCP client on this machine — a name (
PAGESPACE_KEY) only resolves against the OS keychain of the same machine and OS user that minted it, so this option requires the MCP client to run there too. Mint a key and reference it by name:pagespace keys create --drive <id> --role member --name agent, then"env": { "PAGESPACE_KEY": "agent" }as above. No secret ever appears in the config file. - MCP client on another machine / CI — get a raw token (
keys create … --show-token, or Settings → MCP in the app) and set"env": { "PAGESPACE_TOKEN": "mcp_…" }. - Never your
logincredential — it's personal, and it has no content access anyway.
A scoped key limits what the server will allow; it is not an isolation boundary for an agent with shell access on your machine. See Agent access for the threat model, and the PageSpace MCP docs for client-by-client setup.
Coming from the standalone pagespace-mcp npm package? It's deprecated in favor of this one
(the pagespace-mcp bin in this package is not) — see
Migrating from pagespace-mcp.
Environment variables
| Variable | Purpose |
|---|---|
| PAGESPACE_TOKEN | Bearer credential; same precedence slot as --token. |
| PAGESPACE_KEY | Stored key name to use; same precedence slot as --key. |
| PAGESPACE_API_URL | API host; same precedence slot as --host. Defaults to https://pagespace.ai. |
| PAGESPACE_TIMEOUT_MS | Request deadline in milliseconds; same precedence slot as --timeout (which takes seconds), and the way to raise it for pagespace mcp, which builds the same client. Unset leaves each operation on its own default. |
| PAGESPACE_PROFILE | Deprecated alias for PAGESPACE_KEY (pre-1.5 name); warns on stderr. |
| PAGESPACE_AUTH_TOKEN | Deprecated alias for PAGESPACE_TOKEN (old pagespace-mcp compatibility); warns on stderr. |
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success. |
| 1 | API or runtime error (network failure, server error, authentication rejected). |
| 2 | Usage error (bad flags, unknown command). |
Notes
- Upgrading from 0.1.x:
--profileis now--key, andkeys create --save-as-profileis now--name— both old flags error with a pointer to the new name.PAGESPACE_PROFILEstill works as a deprecated alias forPAGESPACE_KEY. Stored credentials are untouched: every key (and your login) minted by an earlier version keeps working under the same name. - Credential safety: no token is ever printed except behind the explicit
--show-tokenopt-in — not in output, not in errors, not in logs.--jsonmode writes nothing to stdout but the JSON payload. Credentials live in the OS keychain (chmod-0600 file fallback); the active-key pointer is just a name, never a secret. - Pure core: argv parsing, config resolution, and routing are pure functions over plain
data. The pieces that must touch the outside world — the bin entrypoint, browser launching,
and credential storage — are isolated behind injected interfaces rather than folded into the
core; handlers receive an injected
{ sdk, stdout, stderr, env, credentialStore }context, which is why the routing/parsing/resolution core is testable without a network.
See also
@pagespace/sdk— the typed client this CLI is a verb layer over.- PageSpace MCP integration docs
- Agent access & isolation
- Migrating from
pagespace-mcp - CHANGELOG
