The bridge
Projects & path containment
The project registry is the reason the cloud can never name a directory. It lives on your Mac, it is the only place paths exist, and it is what turns an opaque id into a folder an agent may work in.
The registry is local#
~/.pagr/projects.json maps each proj_… id to an absolute path on your machine. The control plane holds the id, the display name, the aliases and a repository hint — and nothing else.
This is what makes the command protocol safe to keep open: a command carrying a filesystem path fails schema validation before the daemon does anything with it, and an id that is not in your registry resolves to nothing.
Registering a project#
cd ~/code/checkout-apipagr project add . --name checkout-api --alias checkout,apiRegistration checks, in order, that the path:
- exists
- resolves — the stored path is the
realpath, with symlinks followed, so a symlink cannot later be repointed somewhere else - is a directory
- is not one of the refused roots below
- is a git repository, unless you pass
--allow-non-git - is not already registered under another id.
Any of those failing exits 5 with a hint that names the fix. If the daemon is running, the registration goes through it and the cloud is told the id and name immediately; if it is not, the record is written locally and the cloud learns about it on the next connection.
Refused paths#
Some directories are never a project checkout, and the registry says so:
| Path | Rule |
|---|---|
/ | Refused exactly |
/System, /private/etc, /etc, /usr, /bin, /sbin | Refused, along with everything underneath them |
/Library | Refused exactly |
| Your home directory | Refused exactly — registering ~ would put every file you own inside one project root |
~/.pagr | Refused, along with everything underneath it |
Names and aliases#
The display name is what you will say out loud in a message, so pick the thing you actually call the repository. It defaults to the folder name and is capped at 80 characters.
Aliases are the shortcuts — up to ten per project. They are what make “start codex on checkout” resolve when the project is called checkout-api. Matching is case-insensitive, on the display name first and then on any alias.
If the folder is a git repository, the bridge also reads .git/config directly — never by shelling out to git — for the origin host, the repository name and the default branch. That hint is sent to the cloud so it can tell two projects with the same name apart.
Path containment#
Registering a folder does not make everything reachable from inside it reachable. Whenever the bridge is asked to touch a path on behalf of a project it runs a containment check:
- the candidate is resolved against the project root
- symlinks are followed with
realpath— for a path that does not exist yet, the nearest existing ancestor is resolved and the remainder appended - the result must still be under the project root. A symlink that escapes it is rejected.
Where roots nest, the deepest matching root wins, and a relative path never matches a project at all.
Containment is a floor, not a ceiling: an agent that asks to write outside its project also trips the touchesOutsideProject hint, which puts the request straight into Tier C. See approvals.
Removing a project#
pagr project remove checkout✓ removed checkout-api (proj_1b7e05c93d6a4f2e8c04b7a91f3d2e56)You can pass the id, the display name or any alias. The cloud can also ask for a removal with the project.remove command — that too only forgets an id.
pagr logout keeps your registry so re-pairing does not mean re-registering everything. pagr logout --purge and pagr uninstall drop it.