Repository access decides who can see a repo at all. Path permissions change that inside the tree, in both directions: hide a directory from most readers, or publish one directory of a private repo to the world.
They're declared in a file in the repo โ .oak/PERMISSIONS, shaped like CODEOWNERS โ and always read from the tip of main.
The file#
# Anything not listed is readable by anyone who can read the repo.
infra/secrets/** @zdgeier @group/sec-team
scripts/ @group/ops
scripts/public/ * # re-opened underneath a locked parent
vault/ # no principals = org owners and admins only
[Open source] # a label, for the settings UI
cli/ @public # published to the world
Each line is a pattern followed by the principals allowed to read what it matches.
| Principal | Means |
|---|---|
@alice | The user alice. |
@group/ops | Every member of the organization's ops group. |
* | Everyone who can already read the repo. |
@public | Everyone, including people with no access to the repo at all. |
| (none) | Organization owners and admins only. |
Owners and admins always have full access, whatever the file says.
Matching#
Patterns work like .gitignore, and the last matching line wins.
foo/matches the directoryfooand everything in it;foomatches a file or directory namedfoo.- A pattern with no
/matches at any depth โ*.pemcoverscerts/prod.pem. A pattern containing/is anchored to the repo root, as is one starting with/. *doesn't cross/;**does.#starts a comment;\#is a literal#, and\a literal space.[Section name]lines just label the entries after them in the UI. They don't affect matching.
Last-match-wins is what makes re-opening read naturally: put the narrower line after the one that locks its parent. It also means order matters โ moving a broad line below a narrow one changes what the narrow one does.
Restricting paths#
For someone who can read the repo, a path no line matches is readable, and lines take access away.
What a restricted path looks like to someone without access:
- Contents are withheld; names and sizes stay visible. Oak's trees are content-addressed and verified by the client, so the server can't hide entries from
oak clone,oak pull, or a mount without breaking that verification. Restricted files arrive empty or absent, and the CLI tells you which were withheld. - The web UI, zip downloads, the
git clonesnapshot, the GitHub mirror, and Sites leave restricted entries out entirely. - Writes are refused. A push or revert that changes a restricted path is rejected unless the pusher has access. Merging
maininto your branch withoak pullis fine โ carrying the restricted files along unchanged isn't a write. - If identical content also lives at a path you can read, you can read those bytes there.
Publishing paths with @public#
@public runs the file the other way, for people who have no access to the repo. For them, a path is hidden unless a line grants it to @public. That's how one private repo can contain a genuinely open-source subtree โ and it fails safe: forget a rule, and too little is published, never too much.
Outsiders can read a published subtree:
- in the web UI (file tree, file view, zip download), and
- with stock Git:
git clone https://oak.space/<org>/<repo>.gitreturns only the public paths.
They can't use oak clone, oak pull, or oak mount โ that protocol has to send whole trees, which would reveal the names and sizes of everything private. Branches, commits, diffs, and CI also stay private. And a private repo with public paths isn't listed anywhere; people reach it by direct link.
Who can change the file#
- The file is read from
main, never from the branch being served โ a branch can't grant itself access. - Once a repo has a
.oak/PERMISSIONS, only organization owners and admins can change it. (Before one exists, anyone with write access can create it, so someone can set it up.)
Editing it#
From the web: the repo's Settings โ Path permissions tab shows the server's own reading of the file โ every entry, who it grants, any warnings, and whether enforcement is on โ and has an editor for people with write access. Saving proposes a branch with the change rather than writing main, so it lands through a normal merge.
From the CLI: it's an ordinary file; commit it, push, and merge like any other change.
CLI versions up to 0.105.0 skip
.oak/PERMISSIONSwhen committing. Ifoak statusdoesn't show your edit,oak upgrade, or make the change from the web editor.
Over the API: GET /api/{owner}/{name}/path-permissions returns the parsed policy; POST โฆ/path-permissions/propose proposes a new file as a branch. See the API reference.
When something's wrong with the file#
Every failure leans toward denying access:
| Situation | Result |
|---|---|
| A pattern can't be parsed | That line is dropped (shown as a warning in Settings). |
| A user or group doesn't exist | It grants no one. |
| The file is over 64 KiB or isn't valid UTF-8 | Everything is locked to owners and admins. |
| There's no file | No restrictions. |
Path permissions and CI#
CI runs check out the whole tree, restricted paths included โ otherwise a repo with a private half couldn't build itself. To keep that safe, a run's logs and status are only visible to people who aren't restricted from anything in the repo. Workflow files are always read in full, even if .oak/workflows/ is itself restricted.