Some folders on a Mac are protected. A program that reads one of them needs Full Disk Access. That setting is not a permission on one folder. It opens everything macOS protects at once.
The need is usually one folder. A program that wants to read the Voice Memos library would need Full Disk Access to do it directly. Granted to that program, the permission also reaches every interpreter or runtime it loads, every package installed beside it, and whatever those packages load later. None of that asked for the permission.
paranco is the smaller thing to grant. It reads one folder per route and writes into an ordinary folder. The program that consumes the files is granted nothing, and keeps working as before.
Two things macOS does that shaped this tool.
Both were measured. The first is why paranco is an application and not a script. The second is why it tells an empty folder from a refused one.
Finding one
An access is charged to whoever asked.
macOS charges an access to the process that asked for it. Drive Finder or System Events from a terminal, and the terminal's permission is what gets tested, not theirs. A permission cannot be borrowed by driving something that has it.
So the holder has to be an application with its own identity, started by launchd or by a person. That is Paranco.app. Its agent runs the application's own executable with --agent, so one grant serves both the window and the agent. The command line runs the same core from a terminal, so it is charged the terminal's permission. On a protected folder, paranco check says refused even after the app was granted. That is macOS attributing the access correctly.
Finding two
A refusal that arrives as an empty folder.
Asked through System Events, a protected folder does not say no. It says it is empty.
16
An ordinary folder with sixteen files, asked through System Events. The count it has.
0
The Voice Memos library, asked the same way from a process without the permission. Not an error and not a refusal. Zero.
Zero is a plausible answer for a library, which makes it the most misleading one. So every folder paranco checks gets one of three answers, and a refusal carries the sentence a person needs.
Answer
What it means
readable
The folder can be listed, and the count is real. Zero here is a real zero.
refused
The folder is there, and macOS is not letting this process list it. The message names the folder and the setting to change.
missing
There is no folder at that path.
macOS lets a program ask whether a protected folder exists. It is the listing that is refused, and that is what makes the three cases separable.
A list, not a file, decides what can be read.
Four facts explain the design: what the list is, what a destination must be, what the copy does, and what a program running as you can still do.
The list is compiled in
A route names its source by identifier, never by path. The identifier is resolved only against Source.builtIn, a list written into the source code. Today it has one entry, voice-memos. Nothing on the machine can add to it: not a file, not the window, not the command line. Adding a folder means editing the source and rebuilding. With an ad-hoc signature, a rebuild also means granting Full Disk Access again, so a reviewer sees the folder being added and a person consents to it again.
A destination is checked before use
It has to be a real folder inside your home, outside ~/Library, not inside any source, and on this Mac's own disk rather than a network or removable one. No symbolic link may appear on its path, and no . or .. component either. All of this is decided before a single byte is written.
The copy takes no shortcuts
Each file lands in a fresh file, created with O_EXCL and O_NOFOLLOW, with mode 0600, no execute bit, and no attributes carried over. It is then renamed into place. Anything already at that name, a symbolic link included, is replaced by the rename and never followed.
What a rogue write still cannot do
A program running as you can still write routes.json. Nothing on the filesystem stops that. What it cannot do is name a source this build does not list, or give a path as a source. It can point a route at another folder in your home, but only one that passes the same destination checks. The list of sources is the boundary, not the file.
paranco has no network code. The source contains no URL session, no socket and no update check. It writes nothing into a source. Mail, Messages and Safari are not on the list, and they will not be added.
Routes, and an agent that runs them.
A route names one source by identifier, one destination folder, and the files it carries. There is no way to say "everything under Library". The program holds a permission that could read anything, and it uses that permission to read one thing.
name what the route is called
sourceID an identifier from the compiled-in list, e.g. "voice-memos"
destination an ordinary folder, created if it is not there
extensions lower-case, without the dot; empty means the source's own list
enabled a disabled route is kept but never run
Lifting a route
A file with the same name and the same size in the destination is already there, and is left alone.
A file whose size differs is copied again. That is what happens when a recording was still being written the first time it was seen.
A file whose size changes between two looks, two seconds apart, is still being written. It is skipped until the next run.
A route carries the regular files at the top level of its source. It skips hidden files and does not go into subfolders.
The destination is the record. What is there is what has been lifted, so there is no database to fall out of step with the folder.
The agent
A launchd agent runs every enabled route every five minutes by default, and once when it is loaded. It runs the application's own executable with --agent. Each route runs once, and the process exits. It prints a line only when something was copied, failed or refused. That output goes to agent.log, beside routes.json.
The agent is installed from the window, not from the command line. The grant belongs to the application bundle, and the window is that bundle.
The route file
Routes live in ~/Library/Application Support/paranco/routes.json. It is a JSON file, so it can be read, diffed and edited with ordinary tools. A route added in the window is the same route the agent and the command line see. A program that reads this file should follow the integration notes.
Install
You need macOS 14 or later, and a Swift 6 toolchain (Xcode 16 or the matching command line tools). There is nothing to download: the app is built from the repository with one script.
git clone https://github.com/nerln/paranco
cd paranco
./build.sh
That leaves Paranco.app in the repository folder. The first build also generates the icon, through make-icon.sh.
1
Grant Full Disk Access
In System Settings, open Privacy & Security, then Full Disk Access, and add Paranco.app. No program that reads the lifted files needs it. It is the only permission its routes need. Other programs can need it for work of their own, for example the log service of paratia, a separate program.
2
Open the app
open Paranco.app
3
Add a route
Pick a source from the list, Voice Memos today, and a destination folder of your own, for example ~/Recordings. The window checks the source and says which of the three answers it got.
4
Install the agent
Install it from the window. From then on, the destination fills itself on each run.
The command line
Build it with swift build -c release --product paranco. That leaves the binary at .build/release/paranco. The commands below use paranco for that binary.
Run from a terminal, the command line is charged the terminal's permission. A protected source therefore reads as refused there, even after Paranco.app was granted. Use the window or the agent for routes out of protected folders. Installing the agent has no command-line form, on purpose.
swift test runs the core's tests. They cover the copy, the "already there" rule, the untouched source, the destination checks, and the difference between a refused folder and an empty one.
Limits
These limits are also in the project's README. Read them before you rely on a guarantee.
macOS only, by nature. The permission and the protected folders are macOS's.
One source on the list today, the Voice Memos library. Any other route into it can be written with paranco add or in the file. A new source is a code change.
The bundle has an ad-hoc signature. Every rebuild is a new application to the privacy list, so Full Disk Access has to be given again after each build.
There is no Developer ID yet, so there is no notarized download. It is built from the repository.
From a terminal, the command line is charged the terminal's permission. It cannot read a protected source, even after the app has been granted access.
A destination must not be an encrypted volume while that volume is closed, for example a folder that paratia protects. The path is then an empty folder on your disk, and files lifted there are written in the clear. paranco cannot see encrypted volumes.
A destination is an ordinary folder. Any program that runs as you can read it. macOS asks for its own permission for Desktop, Documents and Downloads.
The family
paranco is one of two small programs by the same author. Both run on the same Mac. Neither calls the other, and paranco does not read paratia's files.
paratia keeps the folders you choose encrypted, so that a coding agent cannot read them while they are locked, and it keeps a log of who opens or changes the files you watch. It also blocks agents while a folder is open, and it covers two agents that together do what neither may do alone.
paranco holds the one permission that copies files out of a protected folder, so no program that reads the copies has to hold it. It copies in one direction only, along routes you set, and its sources are written into the program rather than into a file.
Each program has its own entry in the macOS list, and each one needs its own permission.
Program
What Full Disk Access is used for
After a new build
paratia Paratia.app
Reading kernel events through its log service
Remove the entry, add it again, then reinstall the service
paranco Paranco.app
Reading the Voice Memos folder
Remove the entry, then add Paranco.app again
They are two separate entries. paranco's permission does not make paratia's unnecessary.
The two programs share one file, routes.json. paratia can read it without changing it, through its command paratia registro rotte-paranco, in the builds that include that command. The command proposes watching the folders the routes write to, so you can see who opens the copied files. Without paratia, paranco does not change.
Neither program needs the other to work.
Read more
READMEHow paranco works, its security model and the install steps.
Integration notesWhat another program can rely on in routes.json and in the destination folder.
A paranco is a block and tackle: pulleys and a rope for lifting a load. The mark is a wheel, a rope and a load. Next to it, paratia is a watertight bulkhead, a wall across a ship. Both are words from a ship.