Skip to content

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.

zsh

$ 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

shell
curl -fsSL https://persistmemory.com/install.sh | sh

Windows

PowerShell
irm https://persistmemory.com/install.ps1 | iex

Any platform

npm, or nothing at all
npm 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.json

It 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 do

The 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 setupsign 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 listthe Spaces you have, with their ids
pm spacewhich 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 --clearstop 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 keepdelete 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 statusis the service healthy
pm versionwhich version this is — the same answer as --version
pm updateinstall the newest version
pm uninstallremove pm from this machine
pm deletedelete every file pm has written here — not your account

Signing in

pm auth loginopen the browser and sign in, then choose a Space
pm auth login --api-keypaste a key instead — for CI and headless machines
pm auth login --no-setupsign in and nothing else, without being asked about a Space
pm auth statuswho am I, and does the server still accept it
pm auth logoutforget 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.mdcapture a file

A session

pmstart 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
/newstart a fresh conversation without leaving
/usagewhat this session has cost

This computer

pm agentanswer file and command requests — reads anywhere, you approve each one
pm agent --root ~/codenarrow it to these folders instead
pm agent --interval 15poll 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 enableturn this machine back on after it was switched off for going quiet
pm requestswhat is waiting for you to approve
pm requests get <id>write a finished one to a file here

Google

pm drive quarter plansearch your Drive by name
pm drive get <id> --out ./plan.pdfdownload one file
pm drive put ./notes.mdsave a file into Drive
pm mail from:priyarecent 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 20the most recent memories
pm get memory mem_01j…one memory, in full
pm list spacesyour 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