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.
Coding memory and conversation controls
Bind your repository to a Space with pm setup, then preview Claude Code hooks before enabling them. Activity capture keeps project paths and bounded test receipts; optional notes mode keeps only user-marked Remember: or Decision: lines. Account, API and Space changes pause capture.
pm hooks claude preview
pm hooks claude enable
pm hooks claude status
pm hooks claude flush
pm hooks claude disableIn interactive chat, use /model to list configured model choices and /reasoning to choose a supported effort. Your preferred model retains eligible fallback; each answer identifies the model used. /branch alternative saves an independent conversation to resume, and /diff notes.md previews a file change without writing. Tab completes slash commands.
Hooks are opt-in and apply to Claude’s main sessions. They do not change the model chosen by Claude, Cursor or another connected AI host. Keep using explicit memory commands for material outside the capture policy.
API change memory · CLI 0.9.12
Keep the baseline. Review what changed.
Track a provider’s public documentation or changelog. Save a first reading, compare it on demand, and review added or removed text before accepting a new baseline. No model call is needed. Snapshots stay in local files.
A failed read preserves successful evidence. Acceptance requires the exact afterHash returned by the check. A text change is a reason to investigate; it does not prove that your application is broken.
pm update
pm watch add postgres \
https://www.postgresql.org/docs/release/
pm watch check postgres --output json
# Review, then copy afterHash from the output
pm watch accept postgres --hash <afterHash>One page, up to 12,000 extracted characters; navigation may be included. Changes beyond that limit are not covered. New checks validate the website now. Local watches run when invoked; saved web watches support optional daily checks and notifications. The API watch guide explains impact analysis and reviewed local repair artifacts. For website interactions, see our crawler and isolated browser.
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.
Connected coding and isolated work
| pm coding list | list your connected coding projects |
| pm coding get <project-id> | inspect explicitly selected repositories and branches |
| pm coding graph <project-id> | read the bounded repository graph and indexed commit evidence |
| pm coding analyze <project-id> | queue analysis of the selected branches |
| pm coding runs <project-id> | list sandboxed coding work and execution availability |
| pm coding run <run-id> | inspect state, exact version and available review artifacts |
| pm coding start <project-id> --repositories <id,id> --prompt "task" | queue feature work with default 300 sandbox seconds and 8 model calls; --kind repair selects repair work |
| pm coding cancel <run-id> --version <version> | cancel the exact observed run version |
| pm coding steer <run-id> --version <version> --message "revision" | queue a new continuation after cancelling the prior attempt |
| pm coding publish <run-id> --version <version> | request draft pull requests for the exact reviewed run |
| pm coding config <project-id> --repository <repository-id> | read the current staging setup/test profile and version |
| pm coding config <project-id> --repository <repository-id> --file <profile.json> | save an explicitly reviewed test profile and manual or after_checks draft-PR mode |
Scanned captures · 0.9.16
| pm jobs scans | review pending scans, including uploads without a processing job or saved document |
| pm jobs scans --state all | include retained completed and replaced scans |
| pm jobs scans --limit 20 --output json | read one page of recovery status and its optional next cursor |
| pm jobs scans --cursor <next-cursor> | read the next page explicitly; listing never submits a retry |
Failed captures · 0.9.15
| pm jobs dead --limit 20 | review failed captures, retry availability and the reason for each refusal |
| pm jobs retry <failed-job-id> | request one eligible retry using the original capture settings; requires owner write access |
| pm jobs retry <failed-job-id> --force | acknowledge that a permanent failure’s cause has been fixed; original Space checks still apply |
| pm jobs get <returned-job-id> | follow the new attempt; acceptance does not mean processing or memory creation has finished |
| pm jobs --status cancelled | view deliberately stopped steps without retrying them |
API change impact · 0.9.14
| pm watch impact <name> --repo "/path/to/repository" | find possible code usages of a local watch’s pending changes |
| pm watch impact --watch=<saved-watch-id> --repo "/path/to/repository" | read a saved web watch and scan your local Git directory |
| pm watch impact <name> --repo . --symbol max_tokens --output json | review an explicit identifier; code stays local and no AI call is made |
Computer results and follow-ups · 0.9.13
| pm ask --conversation <id> --results | read completed computer results without another AI call |
| pm ask --conversation <id> "your next question" | continue with the saved thread, including results that arrived later |
| pm requests get <request-id> | download the file returned by a finished request |
Public sources and API changes · 0.9.12
| pm web read <url> --fresh | validate current content with our crawler |
| pm web read <url> --render always --include main | render JavaScript in our isolated browser and select content |
| pm web browser start <url> | open an isolated interactive browser session |
| pm web browser get <session-id> | inspect visible browser controls and version |
| pm web browser close <session-id> | close the session and release its temporary state |
| pm web search <topic> | find public sources without an LLM call |
| pm watch remote list [--space ID] | list hosted watches shared with web, MCP and connected chat |
| pm watch remote get <watch-id> | inspect saved baseline, pending change and exact version |
| pm watch remote create --name TEXT --url URL --space ID | save a hosted baseline in an active owned Space |
| pm watch remote check <watch-id> --version N | freshly check a hosted source |
| pm watch remote schedule <watch-id> --version N --schedule daily | enable daily checks; optional --notifications true|false updates email alerts |
| pm watch remote accept <watch-id> --version N --hash HASH | accept only the reviewed hosted change |
| pm watch remote remove <watch-id> --version N | delete a hosted watch and its snapshots |
| pm watch add <name> <url> | save a documentation baseline in local files |
| pm watch check <name> --fail-on-change | compare text; exit 3 for a change, 0 when unchanged |
| pm watch accept <name> --hash <afterHash> | make exactly the reviewed change the new baseline |
| pm watch list | show locally tracked sources; --dir selects their storage root |
| pm jobs get <id> | check the processing job returned by a capture |
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