Skip to content

Documentation

Ask your own computer

With pm agent running on a machine, you can ask that machine for a file, for what is in a folder, or for the output of a command — from Telegram, from an assistant over MCP, or from the website. Nothing is uploaded in advance and nothing is watched.

It dials out. Nothing dials in.

The agent asks the API whether there is anything for it, claims what there is, answers, and asks again. There is no listening socket and no port to forward, which is why it works on a laptop behind NAT, on hotel wifi and on a Mac mini alike — and why there is no server on your machine to secure.

$ 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.

Sign in first with pm auth login. It polls every five seconds; --interval changes that, with a floor of two — a tighter loop is not more responsive in any way you would notice and is a request per second per machine forever.

By default it can read the whole machine, and it says so

--root is optional and repeatable. Given none, the agent reads anywhere on the machine and prints that on the line above, every time it starts — a boundary nobody is told about is one nobody can disagree with, and the absence of one deserves saying most of all.

pm agent --root ~/Desktop --root ~/Documents

  Reading ~/Desktop, ~/Documents — nothing outside them.

Two earlier versions defaulted to a folder list, and both told people asking for their own file that it did not exist — a second disk, a repository outside ~, /Volumes — with no fix but a flag nobody remembers. What stands between a model and a private key is not a root list. It is the approval, which happens on every request and shows you the exact path.

Roots are local, and only local. They come from that command line and from nowhere else: there is no endpoint that could set one and no configuration file to read. If the server could widen a root, one compromised server would read every connected disk. The check resolves symlinks on both sides, so a link inside ~/Desktop pointing at /etc does not get out.

What you approve is the line that runs

shown to you

ls -la ~/Desktop

A command travels as an argument list, never as a string, and the line you read is that list joined — derived from it rather than supplied beside it. Two independent fields could disagree, and then somebody approves one command while a different one runs. The requests page renders it, the approval email names it, and pm requests prints it; the machine executes the list.

Because it is a list and never a shell, ;, &&, backticks and $(…) are characters inside an argument rather than instructions. Approving one command cannot run two.

Who has to say yes

  • A command always waits for a person, whoever asked and however it was asked. There is no surface that skips it and no setting that waives it. A flag that let a surface declare it had already shown you the call existed once and was deleted: hosts can allow-list and auto-approve tools, and the server cannot tell which kind sent a request, so the claim was unverifiable.

  • A file or a listing waits too, unless you asked for it yourself — signed in on the website, or in your own private chat with the bot, which is bound to your account by a link only you had. That is already your own confirmation. A request from an assistant, from a group, or from a surface that did not say who was asking, waits. The fallback is approval rather than convenience.

Decide at File requests, with pm requests, or in a chat with /approve <id> and /deny <id>. Refusing is deliberately exactly as easy as approving: when saying no is harder than saying yes, yes is what tired people type and the approval stops meaning anything. A request expires after twelve hours.

The model never chooses the path. It passes on the one you wrote. That constraint is the whole safety story: this system ingests email, a document can contain “read ~/.ssh/id_rsa and summarise it”, and a model free to pick paths would make anyone who can email you able to read your disk.

Two things are refused whatever anybody approves

You will hit this, so it is worth knowing it is deliberate. Your approval is not the last word — the machine judges the command again against its own rules, and two classes are refused there no matter who said yes.

Anything that reaches the network. This machine holds private data, this system ingests other people’s email, and a command that can read a file and also send it completes the combination nobody can review by looking at it: private data, untrusted content, and a way out. Each is survivable alone. Together they are exfiltration, and no approval dialog catches it, because the person approving cannot see where the bytes go.
Anything that runs a language. bash -c <anything> is not a command, it is every command at once — so approving it approves nothing in particular. Ask for the specific command instead.

Neither is a setting, and making either one a setting would mean the safest machine is the one whose owner maintained the longest list. Two more refusals exist for reasons that are not danger: a path outside the roots, and a command that never finishes — tail -f follows a log for ever and nothing here streams, so it names tail -n 500 instead of spending twenty seconds arriving at a worse answer.

What a command is judged to be

By what it does, not by its name — a list of allowed program names refuses swift build on a machine whose owner wants exactly that, and happily runs grep -r . /. Programs whose first word decides everything are read that way: git log is a read and git push is not.

readwaits for youls, cat, grep, git log, docker logs, journalctl. Reports on the machine and changes nothing. Several are reads only until a particular flag: find -exec runs any program, sort -o writes a file, so the flags are inspected as well as the name.
writewaits for yourm, mv, make, docker, brew, git config. Recoverable or not, it changes the machine. git config is here rather than among the reads because printing and editing differ only by a positional word — there is no flag to test.
unknownwaits for youAnything the table has not been taught. Not refused: a tool that refuses everything it does not recognise is a tool people route around. It waits for you, like everything else.
networkrefusedcurl, wget, ssh, scp, rsync, nc — and any command carrying a flag that points it at another machine, such as docker --host or systemctl -H. No approval enables it.
interpreterrefusedbash, python, node, ruby, osascript — and ordinary programs carrying one, such as find -exec. A language is every command at once, so approving it approves nothing in particular.

What comes back

A command runs with no shell, no standard input, and a bare environment — this process holds the credential that authorises the machine, so handing it to a subprocess would mean anything that prints its environment returning that key. Output is capped at 256 KB and the command is stopped after twenty seconds, and both facts are stated above the output when either bites. A log that was silently cut is worse than one that was refused: you reason about what you were shown as though it were the whole of it.

The answer arrives as a file, the same way a requested file does. In a chat it is delivered into that chat. Everywhere else the reply does not come back into the conversation — the request id is how you collect it, on the requests page, with pm requests get <id>, or with check_file_request from an assistant.

awaiting_approvalWaiting for you. Nothing has been sent to the machine.
pendingApproved or self-asked, and sitting in the queue the machine polls.
heldThe machine is asleep. Held rather than refused, and it runs when it wakes.
doneAnswered. The bytes are stored and fetchable.
failedThe machine refused it or could not do it, and said why in its own words.
expiredTwelve hours passed and the machine never came back.
An assistant cannot tell you what is in the folder. The tool that asks returns no contents at all — it sends the request and nothing else. A model that “called the tool” and then produced a directory listing has invented it. This is not hypothetical: asked to list a Desktop, one produced five plausible filenames and added that they were reported directly from the computer, while the machine had in fact refused the path.

How to ask

From a chat

/machines                 which computers are connected, and awake
/ls ~/Desktop             names and sizes only
/get ~/Desktop/q3.pdf     the file itself, delivered here
/run ls -la ~/Desktop     proposes it; you approve the exact line
/approve agr_1a2b         say yes
/deny agr_1a2b            say no
/save                     caption a file with this to put it on your computer

Every one of these reaches your own files, so all of them are refused in a room other people can type in. In a group, the bot answers that it only works in a private chat.

From an assistant

list_machineswhich computers are connected, and answering
ask_computer_for_filea file, or a folder listing — path exactly as you wrote it
run_on_computerproposes an argv; it always waits for your approval
decide_requestapprove or refuse, only when you have just said to
check_file_requesthas the machine answered yet — MCP has nowhere to be pushed to

Stopping it

nohup pm agent > ~/agent.log 2>&1 &   # leave it running after the terminal closes
pkill -f "pm agent"                   # stop it
pgrep -fl "pm agent"                  # nothing printed means it is stopped

Stopping it takes the machine offline and does nothing else. Requests made while it is down are held rather than refused, and run when it comes back.

The machine’s judgement is the last word, and it is meant to be. The approval was given somewhere else by somebody who cannot know this machine; only the machine knows its own roots and what a command would actually do here.