Skip to content
GitOne
Back to gitone.io

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.

One working directorymy-project/
  • .gitone.ymlapp
  • .gitignoreapp
  • README.mdapp
  • src/index.tsapp
  • AGENTS.mdagents
  • CLAUDE.mdagents
  • .agents/skills/review.mdagents
  • .claude/settings.jsonagents
  • .claude/skillslink to ../.agents/skillsagents
.gitone.yml
version: 1
default_branch: main

repositories:
  app:
    visibility: public
    remote: git@github.com:example/app.git
    paths:
      - .gitignore
      - .gitone.yml
      - README.md
      - src/**
.gitone.local.yml
version: 1

repositories:
  agents:
    visibility: private
    remote: git@github.com:example/agents-private.git
    paths:
      - .agents/**
      - .claude/**
      - AGENTS.md
      - CLAUDE.md
  • The agent finds .claude/ where it expects it, and the public remote never sees it.

  • .claude/skills stays 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 app

The 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.

One working directorymy-project/
  • .gitone.ymllibrary
  • .gitignorelibrary
  • LICENSElibrary
  • README.mdlibrary
  • docs/guide.mdlibrary
  • src/lib.golibrary
  • ops/runbook.mdops
  • ops/customers.mdops
.gitone.yml
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/**
.gitone.local.yml
version: 1

repositories:
  ops:
    visibility: private
    push: disabled
    paths:
      - ops/**
  • push: disabled makes gitone push skip ops and refuse it as an explicit target. There is no flag that publishes it anyway.

  • protected_paths is a hard deny that beats ownership: .env can never be staged, not even by the repository whose pattern covers it.

  • require_clean_worktree refuses 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.

One working directorymy-project/
  • .gitone.ymlportfolio
  • .gitignoreportfolio
  • README.mdportfolio
  • site/style.cssportfolio
  • clients/acme/contract.mdclients
.gitone.yml
version: 1
default_branch: main

repositories:
  portfolio:
    visibility: public
    remote: git@github.com:example/portfolio.git
    paths:
      - .gitignore
      - .gitone.yml
      - README.md
      - site/**
.gitone.local.yml
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.

  • clients exists 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 with PATH001 instead 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.

One working directorymy-project/
  • .gitone.ymlweb
  • .gitignoreweb
  • README.mdweb
  • .github/workflows/ci.ymlweb
  • src/app.tsweb
  • infra/main.tfinfra
.gitone.yml
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/**
.gitone.local.yml
version: 1

repositories:
  infra:
    visibility: private
    push: disabled
    paths:
      - infra/**
  • The workflow stays a normal workflow. It checks out web, which never contained infra/, 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 doctor is read-only, so it is safe at any time.

  • gitone in 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 test

No 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 $?
1

Both 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.

One working directorymy-project/
  • .gitone.ymldotfiles
  • .gitignoredotfiles
  • README.mddotfiles
  • shell/aliases.shdotfiles
  • editor/init.vimdotfiles
  • machine/hosts.confmachine
.gitone.yml
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/**
.gitone.local.yml
version: 1

repositories:
  machine:
    visibility: private
    paths:
      - machine/**
  • machine has no remote at all. It is a real Git repository with real history that simply never leaves the laptop; only pushing it would fail.

  • **/*.key is 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.

One working directorymy-project/
  • .gitone.ymlengine
  • .gitignoreengine
  • LICENSEengine
  • README.mdengine
  • engine/main.cengine
  • assets/hero.pngassets
.gitone.yml
version: 1
default_branch: main

repositories:
  engine:
    visibility: public
    remote: git@github.com:example/engine.git
    paths:
      - .gitignore
      - .gitone.yml
      - LICENSE
      - README.md
      - engine/**
.gitone.local.yml
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, private

List 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
esc

On this page

Elsewhere

↑↓to navigateto select