Six projects, one working directory each.
Every configuration below was written into a real project and checked with the binary before it was published here - repo validate, init, status and add -A all pass, which is the only proof that an ownership map really covers its tree. Copy one and change the names.
Case 01
Keep your AI instructions out of the public repository
The code is public. Your agent skills, prompts and house rules are not - and they have to sit exactly where the agent looks for them: .claude/, AGENTS.md, CLAUDE.md. Moving them breaks the tooling, committing them publishes them.
my-project/.gitone.ymlapp.gitignoreappREADME.mdappsrc/index.tsappAGENTS.mdagentsCLAUDE.mdagents.agents/skills/review.mdagents.claude/settings.jsonagents.claude/skillslink to ../.agents/skillsagents
version: 1
default_branch: main
repositories:
app:
visibility: public
remote: git@github.com:example/app.git
paths:
- .gitignore
- .gitone.yml
- README.md
- src/**version: 1
repositories:
agents:
visibility: private
remote: git@github.com:example/agents-private.git
paths:
- .agents/**
- .claude/**
- AGENTS.md
- CLAUDE.mdThe agent finds
.claude/where it expects it, and the public remote never sees it..claude/skillsstays one symbolic link. GitOne owns the link itself and never walks through it to invent paths below the alias.The link and its target belong to the same repository, which is the rule that makes the alias safe.
What happens if you get it backwards
# .claude/** moved to the public repository, the skills left private
$ gitone add -A
PATH003 .claude/skills: symbolic link to ../.agents/skills: the link target .agents/skills/review.md belongs to agents, not appThe alias would reach across the ownership line, so it is refused before anything is staged.
Case 02
Ship the library, keep the operations private
The library is public and you want contributors. The runbook, the customer list and the incident notes are not public - and they are exactly the files you want open next to the code while you work.
my-project/.gitone.ymllibrary.gitignorelibraryLICENSElibraryREADME.mdlibrarydocs/guide.mdlibrarysrc/lib.golibraryops/runbook.mdopsops/customers.mdops
version: 1
default_branch: main
rules:
protected_paths:
- .env
- secrets/**
push:
require_clean_worktree: true
repositories:
library:
visibility: public
remote: git@github.com:example/library.git
paths:
- .gitignore
- .gitone.yml
- LICENSE
- README.md
- docs/**
- src/**version: 1
repositories:
ops:
visibility: private
push: disabled
paths:
- ops/**push: disabledmakesgitone pushskipopsand refuse it as an explicit target. There is no flag that publishes it anyway.protected_pathsis a hard deny that beats ownership:.envcan never be staged, not even by the repository whose pattern covers it.require_clean_worktreerefuses a push while anything is staged, unstaged or untracked, so you never publish half a change.
Case 03
Two clients, two hosts, one working directory
Your portfolio belongs on GitHub. The client material belongs on the studio’s own GitLab, under an agreement that has nothing to do with GitHub. While you work, both are the same folder.
my-project/.gitone.ymlportfolio.gitignoreportfolioREADME.mdportfoliosite/style.cssportfolioclients/acme/contract.mdclients
version: 1
default_branch: main
repositories:
portfolio:
visibility: public
remote: git@github.com:example/portfolio.git
paths:
- .gitignore
- .gitone.yml
- README.md
- site/**version: 1
repositories:
clients:
visibility: private
remote: git@gitlab.internal.example:studio/clients.git
paths:
- clients/**Remotes are configured per repository. Public work on GitHub, client work on your own host, no third tool in between.
clientsexists only in.gitone.local.yml, which is never committed - so a clone of the portfolio cannot even learn that the repository exists.A new file under
clients/that no pattern covers stops every command withPATH001instead of drifting into the portfolio.
Case 04
Your pipeline does not need to know about GitOne
A managed repository is an ordinary Git repository. CI clones the public one and builds it exactly as before - the private paths are not in that repository at all, so there is nothing to filter, mask or scrub from a build log.
my-project/.gitone.ymlweb.gitignorewebREADME.mdweb.github/workflows/ci.ymlwebsrc/app.tswebinfra/main.tfinfra
version: 1
default_branch: main
rules:
protected_paths:
- .env
- secrets/**
repositories:
web:
visibility: public
remote: git@github.com:example/web.git
paths:
- .github/**
- .gitignore
- .gitone.yml
- README.md
- src/**version: 1
repositories:
infra:
visibility: private
push: disabled
paths:
- infra/**The workflow stays a normal workflow. It checks out
web, which never containedinfra/, so no step has to know that GitOne exists.The check that is worth automating runs where both repositories are still one directory: your machine, before the push.
gitone doctoris read-only, so it is safe at any time.gitonein a CI checkout has nothing to manage - that checkout is a single ordinary repository with a root.git/, which is exactly the shape GitOne refuses to touch. Run the gate locally, not in the pipeline.
The workflow: unchanged
name: ci
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm testNo GitOne step, no allowlist, no path filter. The runner only ever sees the public repository.
The gate: before you push
$ gitone doctor && gitone status --json > /dev/null
# exits 0 when the map still covers the working tree
$ echo "notes" > scratch.txt
$ gitone status --json | jq .issues
[{"code":"PATH001","path":"scratch.txt","detail":"path is not assigned to any repository"}]
$ echo $?
1Both exit non-zero on the first unassigned path, so one line of shell is the whole gate.
Case 05
Share the dotfiles, keep the machine details
Your shell and editor configuration is worth publishing. The hostnames, the work proxy and the paths that only exist on your laptop are not, and splitting the repository means two checkouts of one ~/.config.
my-project/.gitone.ymldotfiles.gitignoredotfilesREADME.mddotfilesshell/aliases.shdotfileseditor/init.vimdotfilesmachine/hosts.confmachine
version: 1
default_branch: main
rules:
protected_paths:
- '**/*.key'
- secrets/**
repositories:
dotfiles:
visibility: public
remote: git@github.com:example/dotfiles.git
paths:
- .gitignore
- .gitone.yml
- README.md
- editor/**
- shell/**version: 1
repositories:
machine:
visibility: private
paths:
- machine/**machinehas no remote at all. It is a real Git repository with real history that simply never leaves the laptop; only pushing it would fail.**/*.keyis protected at any depth, so a key file cannot be staged even if it lands inside a directory the public repository owns.One directory means one
source ~/.config/shell/aliases.sh. Nothing has to be symlinked together from two checkouts.
Case 06
Open engine, licensed assets
The engine is MIT and belongs on GitHub. The art you licensed may not be redistributed, and the licence says so in writing - but the game does not run without it in the same tree.
my-project/.gitone.ymlengine.gitignoreengineLICENSEengineREADME.mdengineengine/main.cengineassets/hero.pngassets
version: 1
default_branch: main
repositories:
engine:
visibility: public
remote: git@github.com:example/engine.git
paths:
- .gitignore
- .gitone.yml
- LICENSE
- README.md
- engine/**version: 1
repositories:
assets:
visibility: private
remote: git@assets.example.com:studio/assets.git
paths:
- assets/**Every push validates the outgoing commits and the resulting tree it produces, so an asset cannot reach the public remote through a rename, a merge or a stray
git add.The two repositories have independent histories. Rewriting the asset repository never touches the engine, and a contributor cloning the engine gets a repository that never contained the art.
Everyone on the team keeps the same working directory. Whoever has no licence simply has no
assets/**in their configuration - and GitOne then tells them the files are unassigned instead of committing them somewhere.
One thing that does not work: a catch-all
Writing ** for "everything else" is the first idea most people have. It is valid configuration, and it makes every single file ambiguous, because patterns from two repositories may overlap but a concrete file may not match both.
repositories:
app:
visibility: public
paths:
- .gitignore
- .gitone.yml
- README.md
- src/**
private:
visibility: private
paths:
- '**' # matches everything above as well$ gitone init
PATH002 .gitignore: path matches app, private
PATH002 .gitone.yml: path matches app, private
PATH002 README.md: path matches app, private
PATH002 src/a.ts: path matches app, privateList the private paths the same way you list the public ones. There is deliberately no priority rule and no most-specific-match rule, so "where does this file go?" stays something you read rather than something you work out.
Every field used above, and every rule these examples lean on, is specified in the usage guide.
Read the usage guide