Command line
Your memory, from a terminal
The same memory your assistants read, reachable from the place you already work. Capture the thing you just worked out, pipe a command’s output straight into it, and ask for any of it back weeks later. One command to install, three to be useful.
$ pm auth login
Opening your browser to sign in.
Signed in to https://api.persistmemory.com as profile "default".
# capture a decision, with the reasoning
$ pm remember "we chose Postgres with pgvector over Pinecone — cost and lock-in"
Accepted for processing. Job job_01j8x…
# and ask for it back, months later
$ pm search "why postgres"
SCORE TYPE TITLE CONTENT 0.914 decision Chose Postgres+pgvector Cost and lock-in were the deciding…
Install
Node 20 or newer is the only requirement, on every platform. The last one installs nothing at all.
macOS and Linux
shellcurl -fsSL https://persistmemory.com/install.sh | sh
Windows
PowerShellirm https://persistmemory.com/install.ps1 | iex
Any platform
npm, or nothing at allnpm install -g @persistmemory/cli # or run it without installing npx @persistmemory/cli auth login
Piping a script into a shell is worth being careful about, so read ours first — install.sh and install.ps1. Both refuse to run rather than guessing when they cannot find a supported platform or a recent enough Node, neither ever escalates to sudo or Administrator on your behalf, and both tell you what to do instead when npm’s global directory is not yours to write to.
pm auth login
Signing in
Two ways, and both are first-class. A browser when there is one, and a key when there is not — because a CLI whose only login needs a browser is a CLI that cannot run in a pipeline.
At a desk, with a browser
pm auth login
Opens your browser, asks you to approve four named permissions, and stores a token that renews itself. Over SSH, where nothing can open, it prints the URL and waits — so the same command works on a server.
Then it keeps going, and asks which Space this folder should use. Signing in is never the thing you wanted; it is the step before it. Pass --no-setup when you want the login on its own.
In CI, with no browser
export PERSISTMEMORY_API_KEY=pm_live_… pm search "last release notes"
An environment variable outranks any stored login, so a runner authenticates without writing a file. Create a key on the settings page, or interactively with pm auth login --api-key, which prompts without echoing so the key never reaches your shell history.
The browser holds the password, not the CLI
Signing in opens your browser and uses OAuth 2.1 with PKCE. Your password is never typed into a terminal and never passes through the CLI. What comes back is a token scoped to what you approved on the consent screen.
A stolen code is worthless on its own
The CLI invents a one-time secret, sends only its hash to the browser, and reveals the secret when redeeming the code. Anything that intercepts the redirect on a shared machine gets a code it cannot exchange.
Credentials are kept apart from settings
Tokens live in ~/.persistmemory/credentials.json at mode 0600. The config file beside it holds only which server you talk to, so the file people paste into a bug report is the harmless one.
pm setup
One folder, one Space
A Space is a filing rule, an audience and a retention policy — not a folder, and nothing is ever only in one. Work stays out of personal, and one client stays out of another. pm setup asks which Space this folder uses and remembers the answer. What a Space actually is.
What it looks like
$ pm setup
PersistMemory setup
You are not signed in yet. Opening your browser.
Signed in and the server answered. ✓
Which Space should this folder use?
1. Work project, 214 memories
2. Personal user, 38 memories
3. Create a new Space
Choose 1-3 [3]: 3
Name for the new Space: Acme
This folder now uses the Space "Acme".
Saved to /Users/you/acme/.persistmemory.jsonIt asks every time, on purpose
Run it again in a folder that already has a Space and it still asks, with the current one as the default — press Enter to keep it. Adding PersistMemory to a second project would otherwise silently inherit the first project’s Space, and you would find out weeks later that one client’s memory had been landing in another’s.
Nothing that needs a person in a script
pm setup --space "Acme" # pick an existing one
pm setup --new-space "Acme" # create it and use it
pm setup --yes # asks nothing — and picks nothing, so
# `pm remember` here is refused until you doThe answer lands in .persistmemory.json beside your code. It holds a Space id and a name — no credential, nothing secret — so committing it gives a colleague the right Space and still makes them sign in as themselves.
After that, every command uses it. --space overrides it for one command and takes a name or an id, and PERSISTMEMORY_SPACE overrides it for a shell.
pm agent
Reach a file on this computer, from anywhere
With the agent running, you can ask for a file — or a folder listing, or the output of a command — from Telegram, from an assistant over MCP, or from the website. It dials out and polls; nothing reaches in, so there is no port to forward and no server on your machine to secure. Nothing is uploaded in advance and nothing is watched.
Start it
Sign in first, then run it. With no --root it reads anywhere on the machine, and it tells you so every time it starts — a boundary nobody is told about is one nobody can disagree with.
$ pm agent Reading anywhere on this machine. You approve every request first, and see the exact path or command before you do. Narrow it with --root if you want to. Answering as Mohammads-MacBook-Air, from: / Nothing outside those folders can be read. Ctrl-C to stop. $ pm agent --root ~/Desktop --root ~/Documents Reading ~/Desktop, ~/Documents — nothing outside them.
It stays in the foreground and polls every five seconds. To leave it running after you close the terminal:
$ nohup pm agent </dev/null >> ~/agent.log 2>&1 &
To stop it — in the foreground, Ctrl-C. Backgrounded:
$ pkill -f "pm agent" # stop it $ pgrep -fl "pm agent" # nothing printed means it is stopped
Stopping it takes the machine offline, and that is all it does. Requests made while it is down are held rather than refused, and run when it comes back.
The approval is the boundary; --root is a narrowing
Every request shows you the exact path, or the exact command, before anything runs. --root narrows one run on top of that, and the roots are local and only local — they come from that command line, there is no endpoint that could set one and no configuration file. A path outside them is refused by the machine itself, and the check resolves symlinks on both sides, so a link inside ~/Desktop pointing at /etc does not get out.
A model asking is not you asking
A request from an assistant waits for you to approve the exact path, at File requests. There is no setting that turns that off, because a setting that turns it off is the one an injected instruction eventually talks somebody into. A request you type yourself, signed in, is already your own confirmation — and a command waits for you whoever asked, with no exception at all.
What this machine will refuse
Your approval is not the last word. The machine judges the command again, and refuses anything that reaches the network — curl, ssh, rsync — or that runs a language — bash, python, node — whatever anybody approved. Private files plus a way out is exfiltration no approval screen can catch, and a language is every command at once. Why, at length.
Then, from a chat
/machines is it awake? /ls ~/Desktop names and sizes only /get ~/Desktop/q3.pdf the file itself /run ls -la ~/Desktop proposes it — you approve the exact line /approve agr_1a2b yes /deny agr_1a2b no
Asleep is not a failure: the request is held and runs when the machine comes back.
Commands
The grammar is pm <verb> <noun>, so a command you have not run yet is usually the one you would guess. pm --help has the full list.
Getting set up
| pm setup | sign in, then choose the Space this folder writes into |
| pm setup --space "Acme" | choose one without being asked — for scripts |
| pm setup --new-space "Acme" | create one and use it |
| pm spaces list | the Spaces you have, with their ids |
| pm space | which Space pm is working in — nothing is captured until one is chosen |
| pm space "Acme" | work in it from now on: notes are filed there and questions are answered from it |
| pm space --clear | stop working in one; nothing is kept until you choose again |
| pm space --email "Work" | where mail forwarded to your ingest address is filed |
| pm spaces create "Acme" | make a new one |
| pm spaces delete "Acme" --memories keep | delete it and keep what was in it — or --memories delete |
| pm spaces merge "Work" "Personal" --name "All" | a new Space holding both; the originals are untouched |
Housekeeping
| pm status | is the service healthy |
| pm version | which version this is — the same answer as --version |
| pm update | install the newest version |
| pm uninstall | remove pm from this machine |
| pm delete | delete every file pm has written here — not your account |
Signing in
| pm auth login | open the browser and sign in, then choose a Space |
| pm auth login --api-key | paste a key instead — for CI and headless machines |
| pm auth login --no-setup | sign in and nothing else, without being asked about a Space |
| pm auth status | who am I, and does the server still accept it |
| pm auth logout | forget the stored credential |
Capturing
| pm remember "we chose Postgres" | capture a line |
| git log -1 | pm remember - | capture whatever is piped in |
| pm remember --file notes.md | capture a file |
A session
| pm | start a session — ask, read files, write answers back |
| pm chat --resume <id> | pick up where you left off |
| /read <path> | read a file into the conversation |
| /capture <path> | read a file AND remember it |
| /remember <text> | remember something directly, mid-session |
| /write <path> | write the last reply to a file — always asks first |
| /new | start a fresh conversation without leaving |
| /usage | what this session has cost |
This computer
| pm agent | answer file and command requests — reads anywhere, you approve each one |
| pm agent --root ~/code | narrow it to these folders instead |
| pm agent --interval 15 | poll less often — the floor is 2 seconds, the default 5 |
| nohup pm agent </dev/null >> ~/agent.log 2>&1 & | leave it running after the terminal closes |
| pkill -f "pm agent" | stop it — the machine simply goes offline |
| pm agent enable | turn this machine back on after it was switched off for going quiet |
| pm requests | what is waiting for you to approve |
| pm requests get <id> | write a finished one to a file here |
| pm drive quarter plan | search your Drive by name |
| pm drive get <id> --out ./plan.pdf | download one file |
| pm drive put ./notes.md | save a file into Drive |
| pm mail from:priya | recent mail, in Gmail's own search syntax |
| pm mail read <id> | one message, with its body |
Asking
| pm search "what did we decide about Postgres" | search your memory |
| pm list memories --limit 20 | the most recent memories |
| pm get memory mem_01j… | one memory, in full |
| pm list spaces | your Spaces |
Readable by a person, parseable by a script
Output is a table when it is going to a terminal and JSON when it is going to a pipe, so both work without your having to know a flag exists. Say --output yaml|csv|tsv when you want something else.
# a table, because this is a terminal $ pm list memories --limit 3 ID TYPE TITLE CONFIDENCE UPDATED mem_01j8x… decision Chose Postgres 0.91 2026-08-25 14:02 mem_01j8w… task Send Northwind PDF 0.78 2026-08-24 09:41
# json, because this is a pipe $ pm search "postgres" | jq -r '.results[].memory.title' Chose Postgres with pgvector # and the exit code says which failure it was $ pm search x; echo $? 3 # not signed in