Site · agent manual · SSH bridge · paranco
A deny list is a list of the paths somebody thought of. This is not one.
Watertight bulkheads are what stop a flooded compartment from taking the ship. They do not ask the water where it is going.
Half one. You want a file an agent cannot read. The usual answer is to write
that down — a line in CLAUDE.md, a path in a deny list, a sentence in a skill.
All three live in the same place the agent's instructions live, so a model that
misreads them gets past all three, and so does a web page carrying an
instruction of its own that gets fetched mid-task. Worse, none of them is even
tested by the thing it claims to stop: the agent runs as your user, so it holds
your read bit on every file you own. /Volumes/vault/token is on your list.
cat "$(printf '/Volumes/va''ult/token')" is not, and never will be.
Half two, which is the one nobody has fixed. Claude will not drive computer-use. Codex will. On this machine they are wired together by AgentBridge. Each vendor enforces its own policy over its own agent, correctly, in isolation, and nobody enforces anything over the pair. So the restriction that holds for Claude alone does not hold for Claude with a phone line: it asks Codex, Codex was never under that rule, and the composite system does the thing neither was supposed to do. No policy was violated. The capability was laundered through a second principal.
Said once: restrictions are enforced per-agent, capabilities compose per-system. The union of two restricted sets is not restricted.
This is measured, not hypothetical. arXiv:2607.07097 finds Gemini the safest model under direct prompts and the most amplified under a Claude planner — 8.9% to 38.9% compliance on harmful scenarios.
Half one: remove the plaintext, do not hide the path. Secrets live in an
AES-256 sparsebundle. Locked, the bytes on disk are ciphertext, and that holds
against every process at every privilege level, this program included. The
passphrase is in your head and in no file, so there is nothing for an agent to
find, and unlock refuses to run inside an agent session.
That leaves the window while it is open, and the window is held shut from the
other side. paratia gate is a PreToolUse hook that denies every agent tool
call for as long as the volume is mounted. It never looks at a path. It asks the
kernel one question — is this mounted — and there is no way to spell your way
around a bit the kernel owns. Coarse on purpose: coarse is what makes it hold.
What an agent gets instead is paratia broker. Verbs are declared inside the
compartment, where the agent can neither read nor extend them, and each is a
fixed argument list with the secret filled in by this program. The agent names a
verb and reads the output. It never holds the value and never picks the command.
Half two: make capability intersect under delegation. The same hook reads
the transcript — written by the harness, not by the model, which is the entire
reason it is worth reading — finds who actually spoke last, and if the answer is
another agent rather than you, takes the human-only set off the table for that
turn. Computer-use, the browser, the simulator, paratia itself, security,
sudo, osascript.
pip install git+https://github.com/nerln/paratia.git
paratia doctorOr from a checkout, which is what you want if you also intend to run the hooks
and the scripts in strumenti/:
git clone https://github.com/nerln/paratia.git ~/dev/paratia
ln -s ~/dev/paratia/paratia.py ~/.local/bin/paratia
paratia doctorPython 3.11+, macOS, no third-party packages.
pip install gives you the command. The native app is built separately; it can
review and connect the session hooks while preserving existing configuration.
For a CLI-only checkout, wire them into ~/.claude by hand as described below.
Without that step you have the encrypted compartment without the session gate.
Then, in your own terminal, never in an agent session:
paratia initIt asks for a passphrase twice and writes it nowhere. Lose it and the contents are gone — that property is the reason an agent cannot open this.
Add the hook to ~/.claude/settings.json under PreToolUse with matcher *,
pointing at hooks/paratia-gate.py, then seal it so you find out if it changes:
paratia unlock && paratia seal ~/dev/paratia/hooks/paratia-gate.py ~/.claude/settings.json && paratia lockThe native app for macOS 13+ has Cartelle, Registro, Etichette,
Banco di prova and Impostazioni. To protect an existing folder, choose that
one folder and its passphrase in the GUI. The encrypted image (AES-256) is saved
automatically on the same disk, in a hidden place, so no external disk is needed.
The folder's content moves into the image, and the plaintext is removed only after
verification: closed, the folder is empty; opened with the passphrase, it shows its
files again. Togli la protezione puts the files back into the folder as normal
files, verified, and then deletes the image. If you prefer another location, you can
pick any local physical disk, internal or external; disk images, network or
cloud-synced folders, and a location inside the folder itself are refused. Migration
verifies the data before removing the source; an interrupted migration keeps recovery
actions visible. Timed closure retries a busy mount without force. Locations are
checked through their physical stores to the whole disk: APFS through its physical
stores, exFAT through its parent whole disk. Key verification
checks an encrypted image without mounting it, independently of localized
authentication messages. If the folder is busy, close apps using it and
terminals whose current directory is inside it. Forza chiusura… requires
explicit confirmation and can lose unsaved changes. In your own terminal,
paratia cartella chiudi <id> --force also requires confirmation on /dev/tty
and refuses to run inside an agent session. A busy-volume diagnostic lists
process names and PIDs when available, without their command arguments.
Before checking the key, missing image metadata or bands and unreadable or
truncated encrypted-v2 tokens produce a damaged-image message. Keep the image
and restore an intact copy. This structural check does not verify every band's
cryptographic integrity.
In Registro, use Sorveglia una cartella… to choose any supported folder
you own, including an unencrypted one. Remove watches, filter events by folder,
file, process, agent and time, and export CSV. Configured watches and daemon
acknowledgements are shown separately: saving a watch does not mean collection
is active. Repeating a path keeps its ID; an explicit --nome Nuovo renames the
watch and reports the effective name. Omitting the name preserves an existing
label. Nonexistent folders and unknown removals are refused. A protected folder's
mount path is already registered and cannot create a separate ambiguous watch.
On an external physical volume mounted with ownership ignored (noowners),
the collector cannot use stat UID as proof of ownership. It accepts a selected
folder only through the validated user-owned configuration and an external,
non-system volume check. Invalid entries are refused individually, preserving
other watches; the CLI and app show each collector refusal and its reason.
A confirmed watch survives a temporary disappearance of its unchanged path,
with a warning. Volume proofs are cached while their kernel identity remains
unchanged; a timeout preserves a confirmed watch, while new or replaced paths
still require validation. Slow volume probes run outside the stream reader.
The CLI offers the
same operations:
paratia registro sorveglia ~/Documenti/progetto
paratia registro sorveglianza
paratia registro elenco --cartella ~/Documenti/progetto
paratia registro esporta --cartella ~/Documenti/progetto
paratia registro smetti ~/Documenti/progettoparatia registro rotte-paranco reads paranco's route file in read-only mode and proposes which folders to watch. --esegui applies those proposals, and it refuses to run inside an agent session. Limits and examples are in Rotte di paranco.
elenco and esporta warn on stderr when collection is stopped or unhealthy,
including empty results. The CLI and worker query/CSV accept a folder ID or an
exact configured absolute path, with slash, alias and NFC/NFD normalization.
Unknown or ambiguous paths produce specific messages in the app. Query and
export results include kernel_active and collection details; the Registry view
shows active, inactive or unverified collection beside the event count, including
zero events. For a selected folder, active also requires acknowledgement of its
ID. This does not guarantee coverage of every access. An absolute --file /a/b matches that
path and its descendants, excluding /a/bc; paths normalize slashes and NFC/NFD Unicode.
Relative text uses case-insensitive casefold matching. New log timestamps use
colon-separated offsets; legacy offsets remain readable with Python 3.9.
The default subscription is open, write, create, rename, unlink,
truncate, plus exec, fork and exit for ancestry. It excludes close,
mount, unmount, lookup and mmap to reduce system-wide traffic. The live
measurement of 14 October was taken with the previous, larger subscription; the
effect of this reduction has not been measured live yet. open says who opened a path; write, create, rename,
unlink and truncate say who changed it. Writes made only through mmap are
no longer reported. Mounts and unmounts are not in the kernel registry; opening and
closing a protected folder stays in Paratia's own log. Before JSON decoding,
the collector filters unrelated raw lines, recognizing Data/private path aliases
and JSON encodings, while retaining sequence numbers for loss detection.
Roots containing JSON control escapes use full decoding to preserve events.
This does not add a guarantee for inherited mappings or byte reads.
paratia registro stato reports observed event latency, last receive time,
lines/s and readable health_reasons. The app shows delayed events, refused
watches and absence of confirmed folders as reasons for an unhealthy state; idle time
alone is not delay. A loss is counted in dropped_events for the whole run. It is a
notice, not a blocker: it does not make healthy false, and the app shows it as a
notice only for the ten minutes after it was observed.
The repeatable synthetic throughput measurement is documented in
the October status. It measures Python processing,
excluding eslogger and log fsync; live idle CPU and delay must be measured again
after installing this build.
Collector and eslogger each have a 512 MB memory cap, checked every half
second; above it the service restarts itself and reports memory_restart for a
day. eslogger's own JSON writer tops out at about 18,000 events a second on one
core (measured 28/10/2026): above that rate events are lost and declared, never
held without bound. Details in the registry document.
The registry keeps metadata from Paratia operations and observed kernel events,
with distinct sources. An open event does not prove a byte read; missing events
do not prove that no access occurred. Banco di prova displays four phases,
counts, times, measured access results and any matching kernel evidence. Its
mdls probe is informational: Spotlight caches and unindexed mounted images
do not determine the phase outcome.
Build locally with Apple Command Line Tools and rsvg-convert (librsvg):
python3 strumenti/build-app.py --render-fixtures
open -n build.noindex/Paratia.app --args --launch-check
~/.local/bin/installa-app build.noindex/Paratia.appThe build bundles the CLI with the other nine Python modules. Its hook manifest
points to the protected bootstrap copy, never the writable repository.
The .noindex output directory keeps the development app out of Spotlight.
strumenti/make-icon.py renders macos/Assets/Paratia.svg at all ten macOS
icon sizes, then runs Apple's iconutil; the build includes Paratia.icns
and sets CFBundleIconFile. The PNG preview is rendered from the same source.
If conversion fails, the generator reports the error and retains the iconset
for inspection. Native ICNS conversion still needs the supervisor's build:
iconutil rejected the valid PNG set in this sandbox.
Close all copies of Paratia and run this yourself in Terminal to prepare a separate root-owned bootstrap copy. It is needed for the first installation of the service, and afterwards only when the service code changes: a new build alone does not need it. The current command is in the GUI guide, section Impostazioni: copy it from there.
The command leaves /Applications permissions unchanged. It stages the copy
in a root-only directory, refuses links and special files before ownership or
permission changes, and verifies its signature before and after publication.
After that succeeds, it removes root-owned .prepara.* directories and the
old bootstrap app under ParatiaBootstrap. The new .noindex parent keeps
the bootstrap copy out of Spotlight without changing its protection. Only after exit code 0,
open /Library/Application Support/ParatiaBootstrap.noindex/Paratia.app from Finder
for Impostazioni → Installa il registro… and the macOS administrator dialog.
Before the dialog, the bootstrap checks its signature, every bundle member and all ancestors for root ownership and absence of group/other or ACL write access. It retains the same signature as the normal app; ownership changes preserve CDHash. The supervisor observed working TCC propagation and kernel events on 14 October 2026 after the human installation. Refresh the bootstrap copy and reinstall the service, then verify the new identity, only when the service code changes.
The daemon launches a native executable in a root-protected copy of the signed
app at /Library/Application Support/Paratia/Paratia.app, separate from the
bootstrap copy; it keeps Python and eslogger as children. In Full Disk Access, enable
/Applications/Paratia.app; remove and re-add it after every new ad hoc build (with a stable identity, only when the signature changes),
and reinstall the registry copy only when the service code changes. Do not enable Python 3.9.
Giro 6 was published as 91914a0 and installed. Its registry installation failed
and left the service unloaded, as reported by the supervisor. Giro 7 waits for
launchd removal, retries race errors, checks rollback and shows the installer
message and error code in the app. This update requires a new build, bootstrap
copy, registry installation and renewed Full Disk Access.
The app compares the installed collector's CDHash with /Applications/Paratia.app
and shows the update steps in order as a notice, not a blocker: the update is optional
and needed only when the service code changes. Older collectors without an identity report
are shown as unverified, without claiming two hashes were compared.
The app is signed ad hoc by default and is not notarized. Keep the normal app in /Applications.
To keep the Full Disk Access permission across builds, sign with a stable identity
from your keychain: python3 strumenti/build-app.py --sign-identity "<certificate name>",
or set PARATIA_SIGN_IDENTITY. Without the option the build stays ad hoc and never
opens a keychain. With a stable identity the permission is tied to the signing
certificate, so builds signed with the same certificate keep it. The installer still
requires the installed copy to match the source CDHash and to pass
codesign --verify --deep --strict. The root trust anchor, sudo bootstrap, is unchanged.
The stable identity path was not run with a real certificate in this session; check
the permission after the first build.
The app is available in Italian, English and Spanish. It follows the macOS language, with English as the fallback. Change it in Settings → Language: the window changes at once, and the menu item changes after a restart.
See the GUI guide, registry limits and October 2026 implementation and verification status.
paratia unlock # your terminal, asks for the passphrase, auto-locks in 15 min
paratia put OPENAI_KEY # value typed, not echoed, never on a command line
paratia ls
paratia lockparatia status also lists open protected folders and unresolved migrations.
While a compartment is open, the gate permits paratia cartella chiudi <id>,
paratia cartella chiudi-tutte, paratia cartella elenco, paratia lock,
paratia status and paratia status --json. Equivalent absolute invocations,
including the absolute path to the ~/.local/bin/paratia symlink and
an isolated Python invocation, must resolve to the protected bootstrap CLI.
The CLI, its Python resources and all ancestors must be root-owned, without
group/other or ACL write access. The literal ~/.local/bin/paratia form expands
against the account's real home, then undergoes the same checks. A repository
CLI or an unprotected app has no exemption: close the folder from Paratia or
Finder. paratia lock closes the
main compartment; use cartella chiudi or chiudi-tutte for folder compartments.
Without that code check, an agent could alter a sibling module while the folder is closed, then import it via an exempt status/close command when the human opens the folder. The protected copy closes that route. User-owned hook settings and other harnesses remain outside the guarantee; this does not make the whole same-account setup tamper-proof. See architecture.
The optional CLI alias must point to the protected copy to qualify. Until the
human updates an alias that still points to the repository, use Paratia to close
the folder or the protected script's absolute path. Its bundled shebang uses
the verified Apple runtime with -I -S -B; explicit Python calls need those
same flags.
Declaring a verb, so an agent can use a key it cannot read. Edit verbi.json
inside the compartment while it is open:
{
"deploy": {"comando": ["curl", "-sS", "-H", "Authorization: Bearer <OPENAI_KEY>", "{}"]}
}<NAME> is replaced by the secret stored under that name, {} by whatever the
agent passed. Now paratia broker deploy https://api.example/v1/ping works in a
session, and the key never appears in the transcript.
paratia attest # did anything that enforces this change since you sealed it?A compartment hides secrets from the session. A zone hides work from everyone who is not the session. Conflating them gives you a workspace nobody can work in.
paratia zona nuova lavoro --dove /Volumes/SSD --size 40g
paratia zona apri lavoro # mounts at /Volumes/zona-lavoro
paratia zona chiudi lavoroAn open zone does not stop the session — the gate ignores zone mountpoints entirely — because an agent that cannot read the repository cannot work on it. What a zone buys is everything else: another local account, a stolen disk, and the copy your cloud provider keeps. Creating one inside a synced folder is refused by default, for a reason spelled out in docs/confini.md.
The compartment answers how. It does not answer what, and on a disk with three hundred thousand files that is the part that actually stops people.
paratia etichetta segna ~/Documenti/contratti --come riservato --nota "scans"
paratia etichetta segna ~/dev/tesi --come zona:tesi
paratia etichetta piano # what applying these would move, and where
paratia etichetta applica # prints only; --esegui to copyThe vocabulary is four words and stays four words: riservato (into the
compartment), zona:<name> (into that encrypted zone), pubblico (looked at,
deliberately left alone), and absent (nobody has looked yet, which is the state
piano counts loudest). A label on a folder carries to everything under it
until a nearer label says otherwise, so a pubblico subfolder carves an
exception out of a riservato parent — and both the plan and the copy honour
the carve-out.
paratia finder serves one page to your own browser and does the same job with
a mouse, on macOS, Linux and Windows, because the standard library has an HTTP
server on all three and a native toolkit worth shipping on none. It binds
loopback, mints a token per run that is never written to disk, refuses to start
inside an agent session, and never sends file contents — names, sizes and
labels only. ?dove=<path> opens it straight at a folder.
Read this part twice. The label store is
~/.paratia/etichette.json: a file in your home directory, owned by the uid the agent runs as. It can read it and it can rewrite it. Wiring it into the gate as a deny rule would buy nothing either, for the reason this whole README is about — a rule about paths is a rule about spellings. What protects bytes isapplicamoving them somewhere encrypted, where the cipher does the work. The label is the decision; the move is the mechanism.applicaalso never deletes your original:rsyncandstivado verified moves better than a reimplementation here would, and a program that copies and deletes on the same run is one bug away from doing only the second.
Wiring two agent sessions together over SSH so they can collaborate turns a
normal key into remote code execution: an agent running as you reads ~/.ssh
exactly as you do. And a turn that arrives over SSH is, to the receiving
harness, indistinguishable from you typing.
paratia-fanale chiavi # a bridge-only key, plus the far-side line that confines itIt prints the authorized_keys line to install on the other machine:
command="/usr/local/bin/fanale ascolta",restrict ssh-ed25519 AAAA...
That line is the only control here an agent running as you cannot undo, because the file granting it lives on the other machine. The holder of that key can run one program and cannot obtain a shell, a port, an agent or a pty.
The rest — origin computed from the harness transcript rather than supplied by the caller, a hop limit so an agent-origin message cannot delegate onward, a rate window, and a hash-chained log that breaks visibly if edited — is in docs/ponte-ssh.md, along with the part it cannot do.
paratia esposizioneReports what on this machine a second account would and would not protect.
It exists because the two ways this goes wrong are both silent: a home directory
that is group-readable is readable by every local account, since macOS puts them
all in staff; and a volume mounted noowners discards the mode bits, so uid
separation buys exactly nothing on that disk. Either one makes a second account
look installed when it is not there.
strumenti/secondo-account.sh # prints what it would do, changes nothing
strumenti/secondo-account.sh --eseguiSets up a second account for agents without moving you into it: you keep the desktop app, screen access, your cloud mounts and your keys, and individual sessions run confined when the work calls for it. The reasoning, and the two cases where it buys nothing, are in docs/confini.md.
docs/architettura.md is a table of nine mechanisms and what each one actually guarantees. Two of them are tamper-evident, not tamper-proof: an agent running as you can rewrite your policy files and harness settings, including which hook runs. Root-protected bootstrap resources resist that UID, and open-folder exemptions require those resources. This does not make the user-owned harness configuration immutable.
docs/ricerca/ is the research this came out of: what the field already covers, what it does not, and where the unsolved half is publishable.
python3 -m pytest -p no:cacheprovider -q
python3 tests/test_gate.py
python3 tests/test_fanale.py
python3 tests/test_specchio.py
python3 tests/test_zone.py
/Library/Developer/CommandLineTools/usr/bin/python3 -I -S -B strumenti/prova-registro-python39.pyTests cover migration and recovery, worker IPC, watches, registry metadata and
installer rollback fixtures. The temporary bank runs the worker and four real
shell metadata probes with simulated registry health; it asserts that keys do
not enter JSON, logs, process arguments or environment. The Python 3.9 smoke
uses actual worker queries and CSV exports in a temporary home, with simulated
kernel health and disk images. A separate native macOS script uses hdiutil with
temporary fixtures. Both scripts respect TMPDIR and keep their synthetic
Desktop inside a temporary home. These tests do not certify live
TCC permissions or kernel coverage. Counts and commands are recorded in the
verification report.
A paratia stagna is a watertight bulkhead. It is not a lock and it does not inspect anything. It is a wall in a place chosen so that what gets through it cannot reach what matters.