Skip to content
GitOne
v0.1 - Linux and macOS·See what it deliberately does not do

Multiple Git repositories. One workspace.

GitOne keeps public code and private files in one working directory and maps every file to exactly one Git repository. When it cannot tell where a file belongs, it stops.

curl -fsSL https://gitone.io/install.sh | sh

Verifies the SHA-256 checksum before it installs anything.Read the script·or download a binary

~/my-project
gitone status
NOTES
visibility: private
unstaged: modified notes/plan.md
WEBSITE
visibility: public
unstaged: modified src/site.css
BRANCHES
notes: main
website: main

Two repositories - one public, one private - in one working directory.

  • Native Git underneath
  • No telemetry, no account
  • Any Git host, mixed
  • Your contributors need nothing
  • Open source, MIT licensed

The problem

Your project is already two projects.

The open source library next to the deployment notes. The public website next to the client contract. The code you ship next to the tasks, agent instructions and internal tooling you never will.

Plain Git gives you two options. Split the project into separate checkouts and lose one coherent working directory - one editor window, one search, one context for your agent. Or keep everything in one repository and rely on nobody ever committing the wrong file.

GitOne is the third option: one directory, several real repositories, and a rule that decides - not a habit that holds.

my-project/
  • .gitone.ymlwebsite
  • .gitone.local.yml
  • .gitone/repositories/
  • README.mdwebsite
  • src/site.csswebsite
  • notes/plan.mdnotes

One directory you work in. No .git/ in the root.

Path ownership

One path. One repository. Always.

Ownership is a map you write, not a heuristic GitOne runs. The grammar is small on purpose: an exact path, * inside one path segment, and at most one ** segment. No priorities, no negation, no most-specific-match rule - a pattern you have to think twice about is a pattern that will surprise you later.

Ownership map
websitepublic
notesprivate

Rejected as invalid configuration

  • src/[ab].mdrejected - CONFIG003
  • docs/**/**/*.mdrejected - CONFIG003
Working directorymy-project/
  • .gitignorewebsite
  • .gitone.ymlwebsite
  • .gitone.local.ymlnever owned
  • .gitone/never owned
  • README.mdwebsite
  • CHANGELOG.mdwebsite
  • src/website
  • site.csswebsite
  • lib/parser.tswebsite
  • notes/notes
  • plan.mdnotes
  • client-contract.mdnotes
.gitone.yml
version: 1
default_branch: main

rules:
  protected_paths:
    - .env
    - secrets/**
  push:
    require_clean_worktree: true

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

repositories:
  notes:
    visibility: private
    push: disabled
    remote: git@internal.example.com:ops/notes.git
    paths:
      - notes/**

None of this is tied to one provider: every repository carries its own origin. GitHub beside GitLab, Codeberg, Gitea, Forgejo, Bitbucket, a company server or a bare SSH path - GitOne runs the same native git for each of them.

Sound familiar?

You have wondered how to do one of these.

Each one is a working directory and a configuration file, not a workflow you have to remember. Four of them are below; the examples page works six out in full.

  • “How do I keep my agent instructions private without moving them out of .claude/?”

    A second repository owns .claude/** and AGENTS.md. The folder stays where the agent looks for it, and the public remote never sees it.

  • “How do I publish the library without publishing the runbook sitting next to it?”

    One repository owns src/** and docs/**, another owns ops/** with push: disabled. No flag publishes the second one.

  • “How do client files stay on our own GitLab while the code goes to GitHub?”

    Remotes are configured per repository. Both halves are one folder while you work, and each half goes where it belongs.

  • “How do I share my dotfiles without sharing my hostnames?”

    The shared half has a remote, the machine-specific half has none. It is still a real repository with real history - it just never leaves the laptop.

Every one of them is a .gitone.yml you can copy and rename.

See six worked configurations

Try it

Play it through, right here.

A real terminal with real recorded GitOne output. Pick a scenario and step through it, or type a command yourself. Unknown commands answer with the same CLI001 the binary would give you.

gitone setup starts with one choice: Wizard, shown here, or Template, which writes commented files you edit yourself. The wizard asks for every repository explicitly and writes nothing before you confirm.

Type a command, or press Tab to take the suggested step.

Steps

Click one to run it

Fail closed

When GitOne doesn’t know, it stops.

Every refusal below is the tool working. Nothing was staged, nothing was committed, no remote was contacted - and the message names the exact path and the exact rule.

UnknownErrorAmbiguousErrorUnsafeErrorUnreachableError
~/my-project
gitone add -A
PATH001 secrets.txt: path is not assigned to any repository

A file that belongs to no repository stops every mutating command - not just this one. Assign it in .gitone.yml or .gitone.local.yml first.

~/my-project
gitone add -A
PATH003 .env: path is protected and must never be committed

rules.protected_paths is a hard deny. It wins even when an ownership pattern also matches the file.

~/my-project
gitone add -A
PATH002 docs/plan.md: path matches notes, website

There is no "most specific match wins" rule. Patterns from two repositories may overlap, but the moment a real file matches both, everything stops.

~/my-project
gitone reset --hard
CLI001 unsupported command: gitone reset --hard

GitOne runs an allowlist. Nothing is forwarded to git, so a command it has not reasoned about cannot reach your repositories.

Never: unknown → public. Never: ambiguous → first match. Never: unreachable → push what we can.

All 22 error codes, with causes and fixes

Why not just…

Submodules, two checkouts, or discipline?

Every alternative below works. Most of them are simpler than GitOne, and if one of them fits your project, use it. Here is the honest comparison, including the three rows GitOne loses.

 SubmodulesTwo checkoutsOne repo + .gitignoreSubtreeGitOne
One working directory - one editor window, one search, one agent contextpartialnoyesyesyes
Split at any path, not only at whole directoriesnonopartialnoyes
Every repository stays an ordinary, independent Git repositoryyesyesnopartialyes
The wrong file in the wrong repository is technically impossiblenoyesnonoyes
Private paths can never enter public historypartialyesnonoyes
Outside contributors need no extra toolyesyesyesyesyes
Brings your existing history alongyesyesyesyesno
Runs on Windowsyesyesyesyesno
Full Git surface - merge, rebase, stash, tagyesyesyesyesno

GitOne trades part of Git’s surface for a check Git does not make. The row that matters is the one in the middle: with every other approach, keeping the private file out of the public repository is a habit. With GitOne it is a rule the tool enforces before it touches a remote.

Not compared: git worktree. It gives you several working directories for branches of one repository - a different problem.

Compatibility

You already know the commands.

GitOne mirrors Git’s spelling, flags and defaults, because underneath every one of these is a native git invocation. Hooks, credential helpers, signing and Git LFS keep working exactly as they do today.

What you type today
What you type with GitOne
git statusgitone status
git add -Agitone add -A
git commit -m "…"gitone commit -m "…"
git diff --stagedgitone diff --staged
git loggit1 log
git branch feature/authgit1 branch feature/auth
git switch feature/authgit1 switch feature/auth
git fetchgit1 fetch
git pullg1 pull
git pushg1 push
git clone <url>g1 clone <url>
git restore <path>g1 restore <path>

Three spellings, one binary.

Install the shorter names next to gitone and use whichever you like. They are plain symlinks - nothing extra runs.

  • $gitone statusthe full name
  • $git1 statusshorter
  • $g1 statusshortest

Anything outside the allowlist fails with CLI001 and is never forwarded to git.

$gitone reset --hard
CLI001 unsupported command

Adoption

Works in your existing project.

A directory that is already one ordinary Git repository becomes a GitOne project with a single command. Your files are never touched - only the layout changes.

  • Your files stay exactly where they are

    Only the two idempotent .gitignore entries are added. Not one managed file is moved, renamed or rewritten.

  • Your history is copied and verified first

    The complete old .git/ is copied to a timestamped directory and checked as a usable Git repository before the root .git/ is removed.

  • GitOne never deletes the backup

    Nothing in GitOne moves, prunes or removes it - clearing it out is your decision and your command. gitone backup list shows every backup it has made, gitone backup git reads one, and gitone backup restore copies it back to the root .git/.

  • The history is not imported

    The managed repositories start empty, with no commits and no staged files. This is the honest trade: GitOne changes the layout, not the past.

~/my-project
gitone migrate --yes
Migrating the root repository into GitOne.
Root repository: /home/devidevio/my-project/.git
Managed repositories: notes, website
The existing history is NOT imported. The managed repositories start without
commits. Every commit, branch, tag, stash, hook, remote and Git setting of the
root repository remains available only in the backup below .gitone/migration-backup/.
Your files themselves are not touched.
WARNING: the working tree has staged, unstaged or untracked changes. Their
Git state, including the index, is kept only in the backup. The files stay as
they are and start as untracked files of the managed repositories.
Migration complete.
✓ notes (main)
✓ website (main)
The original root Git repository was preserved at:
.gitone/migration-backup/20260828T122618Z-3426502608
Inspect it with:
gitone backup git --backup 20260828T122618Z-3426502608 -- log --all
Restore it with:
gitone backup restore --backup 20260828T122618Z-3426502608
Nothing was staged or committed.

Editor

Native Source Control in VS Code.

If you would rather see your changes than type them, the GitOne extension puts every managed repository into VS Code’s own Source Control view - the real one, not a custom panel.

  • One Source Control provider per GitOne project
  • Staged and working-tree groups per managed repository
  • File decorations, gutter changes and HEAD / index / working-tree diffs
  • Stage, Unstage, Commit, Commit & Push, Push, Refresh, Recover, Abort
  • Refreshes automatically after GitOne commands run in an external terminal
  • Multi-root workspaces with distinct GitOne projects

The extension never invokes Git and never reads .gitone/ directly. Everything goes through the same public gitone CLI you use in the terminal.

Branch, pull, fetch, sync, stash, history and discard are not part of this version.

Source Control
GitOne: my-project2
Add site and notes
website - Staged Changes
MREADME.md
Msite.csssrc
notes - Changes
Mplan.mdnotes

The real group names: {repository} - Staged Changes and {repository} - Changes, one pair per managed repository.

Install it

Not in the Marketplace yet

Until the extension is published in the Marketplace it ships as a VSIX with every release. Download it, then install it through GitOne itself.

Download the VSIX
gitone vscode install --vsix ./gitone_v0.1.0_vscode.vsix

AI

Built for humans. Ready for agents.

Your coding agent should not have to know which files are public and which are private. GitOne does. It gives an agent one workspace, one set of commands, machine-readable state - and a hard stop the moment it puts a file somewhere it does not belong.

  • One workspace, not a boundary to reason about

    The agent sees one project. GitOne routes each change to the repository that owns its path.

  • Machine-readable state

    gitone status --json carries a versioned schema. Exit 0 means ordinary changes, exit 1 means the issues array explains why the project is unsafe.

  • A push it cannot do by accident

    Without a terminal, gitone push fails with PUSH001 instead of asking. Publishing takes an explicit --yes.

  • The block GitOne maintains

    gitone agents update writes this block into /AGENTS.md, between fixed markers. Everything outside them is preserved, setup offers it with default no, and a private repository may own the file.

  • The one thing it cannot check

    GitOne never reads file contents, so it cannot see a public file importing a private one. The AGENTS.md block tells the agent to check that itself and to ask you rather than move the path.

gitone status --json
{
  "version": 1,
  "repositories": [
    { "name": "notes",   "visibility": "private", "branch": "main", "ahead": 0, "behind": 0 },
    { "name": "website", "visibility": "public",  "branch": "main", "ahead": 1, "behind": 0 }
  ],
  "changes": [
    { "repository": "website", "state": "staged", "type": "modified", "path": "src/site.css" }
  ],
  "issues": []
}

Drop this into your AGENTS.md, CLAUDE.md or equivalent

Or let GitOne keep it there: gitone agents update writes exactly this block into your AGENTS.md, and gitone agents checks it in CI without changing anything.

AGENTS.md
This project uses GitOne: one working directory, several Git repositories,
ownership decided by path in `.gitone.yml`.

- Use `gitone`, never `git`, for status, staging, committing and pushing.
  There is no `.git/` in the project root.
- `gitone status --json` is the machine-readable project state (schema
  `version: 1`). Exit code 0 means ordinary changes, 1 means the `issues`
  array explains why the project is unsafe.
- Available: `init`, `repo validate`, `repo list`, `status [--porcelain|--json]`,
  `add [-A|-u|<path>...|.]`, `unstage [-A|<path>...|.]`,
  `restore [--staged] <path>...`, `commit [<path>...] -m <message>`,
  `commit [<path>...] -F -`, `diff [all|<repository>] [--staged]`,
  `log [all|<repository>] [-n <count>]`, `branch [<name>]`, `switch <branch>`,
  `fetch [all|<repository>]`, `pull [all|<repository>]`,
  `push [all|<repository>] [--yes]`, `show --repository <name>
  --source <head|index> -- <path>`, `vscode info --json`, `recover`, `abort`,
  `doctor`, `agents`, `agents update`,
  `vscode install [--vsix <path>] [--force]`. `gitone <command>
  --help` prints the accepted syntax. Everything
  else fails with `CLI001`; do not fall back to plain `git` to work around it.
- Never create, edit or commit `.gitone/` or `.gitone.local.yml`. Never move a
  path across repository boundaries to make an error go away.
- A new file must be added to the `paths` of exactly one repository in
  `.gitone.yml` (public) or `.gitone.local.yml` (private) before it can be
  staged. `PATH001` means unassigned, `PATH002` means assigned twice.
- Repository boundaries are build boundaries. Before one file references
  another - an import, an include, a build input - check whether both paths
  have the same owner, and check the references you added again before you
  hand the work back. A reference across repositories works in your working
  directory and breaks in any checkout that has only one of them, such as CI.
  GitOne never reads file contents, so it cannot catch this: report it and ask
  a human rather than moving the path.
- `commit` with explicit paths commits the current working-tree version of
  exactly those managed files, like native `git commit <path>`, and leaves
  every other staged change staged. A file Git does not know yet has to be
  staged once before it can be selected.
- A path covered by `rules.protected_paths` must never be staged or committed;
  ownership does not override that hard deny.
- Valid patterns are exact paths such as `README.md`, `*` inside one path
  segment and at most one `**` segment, as in `src/**`, `docs/**/*.md` or
  `**/*.md`. Duplicate patterns are an error; overlapping ones are not, but a
  concrete path matching two repositories is `PATH002`.
- Non-interactive pushes require `gitone push --yes`; without a terminal a
  push fails with `PUSH001` instead of asking. Ask a human before pushing.
  `push: disabled` repositories are never published, and a configured
  `require_clean_worktree` rule must also pass.
- On `LOCK001` wait and retry. On `REC001`, `recover` finishes an interrupted
  add, unstage, restore or commit and `abort` undoes it. For an interrupted push both
  commands only report what reached the remotes. Never delete
  `.gitone/recovery/` by hand.
- `gitone doctor` is read-only and safe to run at any time, including while
  another operation holds the lock.

Scope

What GitOne deliberately does not do.

The list below is not a roadmap of regrets - most of it is missing because supporting it safely takes design work that has not happened yet, and shipping it unsafely would break the one rule the tool is built around.

Not implemented, rejected with CLI001

  • reset
  • tag
  • merge
  • rebase
  • stash
  • bisect
  • arbitrary Git pass-through

Partially implemented

  • branch only lists and creates - it never deletes or renames
  • switch only moves to a branch that already exists everywhere - it never creates one
  • restore only reads the index, never a revision
  • pull is fast-forward only, with no flag to change it
  • symbolic links only as a safe internal alias - absolute, escaping, dangling and cross-repository links are refused

Out of scope (for now)

  • Windows
  • Submodules
  • Importing the history of an existing root .git/
  • Package-manager distribution (brew, apt, …)
  • IDE integrations other than VS Code

Things it will never be

  • A secret scanner - GitOne never reads your file contents
  • An access-control boundary between files on your own machine
  • A replacement for Git

If one of these is a blocker for you today, GitOne is not ready for your project yet. That is a better outcome than finding out after you migrated.

Install

Four ways in. Pick yours.

Requirements: Git 2.28 or newer. Building from source additionally needs Go 1.25 or newer. Linux and macOS, amd64 and arm64.

The one-liner

Detects your platform, downloads the matching binary from the latest GitHub release, verifies its SHA-256 against the published checksums.txt, and refuses to install anything that does not match.

It installs to ~/.local/bin and never calls sudo. Set GITONE_INSTALL_DIR to change that, or GITONE_VERSION to pin a release.

curl -fsSL https://gitone.io/install.sh | sh

Download and verify it yourself

Every release publishes binaries for Linux and macOS on amd64 and arm64, together with checksums.txt. Verify before installing - the script above does exactly this, only automatically.

On macOS use shasum -a 256 -c --ignore-missing checksums.txt.

sha256sum --check --ignore-missing checksums.txt
install -m 0755 gitone_v0.1.0_linux_amd64 /usr/local/bin/gitone

If you already have Go

Installs the binary into your GOBIN from the module proxy.

go install github.com/devidevio/gitone/cmd/gitone@latest

Build it yourself

The whole tool is one Go binary with no runtime dependencies beyond git itself.

git clone https://github.com/devidevio/gitone.git
cd gitone
go build -o gitone ./cmd/gitone
install -m 0755 gitone /usr/local/bin/gitone

Optional: the shorter commands

Plain symlinks next to the installed binary. gitone setup does not manage them, because setup is project-specific while this is a one-time choice for your machine.

ln -s "$(command -v gitone)" "$(dirname "$(command -v gitone)")/git1"
ln -s "$(command -v gitone)" "$(dirname "$(command -v gitone)")/g1"

Then check it worked

Run this inside a project. doctor is read-only and safe at any time, even while another operation holds the lock.

gitone --version
gitone doctor

Requires Git 2.28+ · Linux and macOS, amd64 and arm64 · Go 1.25+ to build from source

FAQ

The questions a sceptic asks.

Do contributors need GitOne to contribute to my public repository?

No. This is a fundamental requirement of the design. Your public repository stays a completely standard Git repository - fork, clone, branch, commit, push, pull request, all without GitOne installed and without any knowledge that it exists.

What happens if I remove GitOne?

Nothing breaks. Each managed repository under .gitone/repositories/ is an ordinary Git directory with its own metadata, index, HEAD, refs and hooks. Point plain git at one with --git-dir and it behaves exactly as it always did. There is no proprietary format and no lock-in.

Does gitone migrate bring my existing history along?

No, and that is a decision rather than an unfinished feature. Splitting one history along path ownership rewrites every commit, and each commit that touched two repositories becomes two unrelated commits describing something that never happened. It also cannot deliver what it appears to promise: if the repository was ever pushed, the old objects stay on the forge no matter what you rewrite locally. So migration converts the project layout, not the history. The managed repositories start empty, and your complete old repository - every commit, branch, tag, stash, reflog, hook and remote - is copied to a verified backup under .gitone/migration-backup/ that GitOne never removes, readable with gitone backup git -- log --all and restorable with gitone backup restore. If you do want a split history, rewrite it with a dedicated tool before you migrate.

Does GitOne scan for secrets?

No. It never reads your file contents. There is no secret scanning, no data classification and no entropy check. A private key inside src/ is published like any other owned file unless its path is explicitly covered by rules.protected_paths. GitOne enforces where a path may be published - not what is inside it.

Does it protect my private files from other programs on my machine?

No, and it is important to be clear about this. Everything sits in one directory that your shell, editor, backup tool and every other local program can read. GitOne is not an access-control boundary. It decides which repository may publish a path, which is a boundary that matters when something leaves your machine.

Can I commit .gitone.local.yml?

The file itself, no - it is a reserved path. gitone add .gitone.local.yml refuses with PATH003, listing it in a repository is CONFIG003, and if it reaches an index another way gitone doctor reports it. Setup writes it into .gitignore for the same reason. What you can do is declare those repositories in the committed .gitone.yml instead, and sometimes that is the right call - a team where everyone should get the same repositories, for example. Just be clear about the price: their names, remote URLs and path patterns become readable for everyone who clones, which sketches your internal structure even though not one private file is published. That is a judgement only you can make, and the default is the careful one.

Does it run on Windows?

Not yet. Linux and macOS, amd64 and arm64. Windows is deferred rather than rejected, but nothing is promised.

Can I use plain git inside a GitOne project?

There is no .git/ in the project root, so bare git commands find nothing. You can point Git at a managed repository with --git-dir and it will work - but every safety check GitOne performs is bypassed when you do, which is exactly the situation the tool exists to prevent. Use it to inspect, not to publish.

Can I run Git commands that GitOne does not support?

Yes, with plain Git against one managed repository at a time: git --git-dir=.gitone/repositories/{name} {command}. GitOne rejects unsupported commands instead of forwarding them. Running plain Git bypasses GitOne’s safety checks and multi-repository coordination. Commands that modify branches, refs, indexes or working-tree files can leave the managed repositories out of sync. Use this only when you understand the resulting state, then check the project with gitone status and gitone doctor.

Do hooks, LFS, signing and credential helpers still work?

Yes. Every operation is a native git invocation against a real repository, so your Git configuration, hooks, credential helpers, commit signing and Git LFS filters all run as they normally do. GitOne stores no credentials of its own.

What about merge, rebase, stash, reset and tag?

Not yet. They are rejected with CLI001 rather than forwarded to Git, because coordinating them across several repositories safely is design work that has not happened yet. Resolving a diverged branch with plain Git and then returning to GitOne is the supported path today.

Are submodules supported?

No. Submodules are not supported at all, and a nested or bare Git repository inside a managed path is reported as PATH003 rather than silently handled.

Are symlinks supported?

Only as a deliberate alias inside one repository, such as .claude/skills -> ../.agents/skills. The stored target has to be relative and non-empty, resolve inside the project, exist as a regular file or a real directory, be reached without passing through another link, and belong to the same repository as the link. A directory target needs at least one relevant path below it, all owned by the same repository. GitOne owns the link path itself and never traverses it. Absolute, escaping, dangling, cross-repository, chained and cyclic links are PATH003, and so is any link that is or reaches .gitone.yml, .gitone.local.yml, .gitignore, .gitone/, .git/ or a protected path - GitOne never opens such a target.

What if a file matches two repositories?

The operation stops with PATH002. Duplicate patterns are rejected earlier as invalid configuration with CONFIG003, but patterns from two repositories are allowed to overlap in theory - the error lands on the concrete file, not on a guess about one. There is no priority rule and no most-specific-match rule - deliberately, because either one turns "where does this file go?" into something you have to work out instead of read.

Telemetry, tracking, accounts?

The tool has none. GitOne runs entirely on your machine with no telemetry, no update check and no account, and contacts nothing but the Git remotes you configured yourself.

Can one repository live on GitHub and another on a private GitLab?

Yes, and not only those two. Remotes are configured per repository, so GitHub, GitLab, Codeberg, Gitea, Forgejo, Bitbucket, a company server or a bare SSH path can sit side by side in one working directory - GitOne calls the same native git for each. Only origin is used by fetch, pull and push. A repository without a remote works for every local command; only pushing it fails.

Is this a monorepo tool like Nx or Turborepo?

No. Those orchestrate builds, tasks and dependencies inside one repository. GitOne does not know what a build is. It decides which of several repositories owns which path, and it is happy to sit underneath any build tool you like.

How do AI agents use it?

They use gitone instead of git, read gitone status --json for machine-readable state, and get a hard refusal instead of a guess when a file is unassigned. There is a ready-made instruction block above that you can paste into your AGENTS.md or CLAUDE.md.

What license is it under?

MIT. GitOne is open source and the underlying repositories stay compatible with standard Git, so public contributions work exactly as contributors expect.

Open source

Built by one developer, for developers with the same problem.

GitOne started because a single project needed to be two: source code that belonged on a public remote, and tasks, specifications and agent instructions that absolutely did not. Splitting it into two checkouts meant losing one working directory. Keeping it in one repository meant trusting a .gitignore and a good memory.

Neither is a system. So the rule became a file, and the file became a tool that refuses when the rule does not decide.

Star it on GitHub

The cheapest way to tell one developer that this was worth building.

Contribute

Issues, edge cases and honest criticism are all useful - especially the edge cases, because this is a tool whose whole value is not being surprising.

Sponsor the work

The tool is free and stays free. Sponsoring pays for the hours behind it, and puts your name on the project.

Open source friends

GitOne is a thin layer over work other people did first. These are the projects it stands on, and what each one carries.

esc

On this page

Elsewhere

↑↓to navigateto select