Git History Cutoff¶
Limit how much git history the assistant container can see. When a cutoff is set,
Nyia Keeper mounts a protected, object-isolated shallow .git over the real one,
so the assistant has recent history (to work effectively) but cannot read commits
before the cutoff — where un-rotated secrets may still live in old blobs.
Threat model: an assistant that can run
git log -p/git cat-fileover full history can read secrets that were committed and later removed. The cutoff removes those objects from what the container can reach. Network egress is not part of this threat model (a user cannot exfiltrate by accident); see the egress policy feature for that.
Quick start¶
# Set a cutoff date (anything before it is hidden from the assistant).
nyia config project git_history_cutoff=2025-01-01
# Launch as usual — the protected shallow .git is built and mounted automatically.
nyia-claude
# After the session, review what the assistant committed:
nyia git-history status
nyia git-history reconcile # read-only review (diff vs your real branch)
nyia git-history reconcile --apply # import into refs/nyia-shallow/<branch>
How it works¶
- Build — before the container starts, nyia clones the repo with
git clone --no-local --shallow-since=<cutoff>overfile://.--no-localis load-bearing: a plain local clone would hardlink or setobjects/info/alternatesback to the full object store, re-exposing every object. The origin remote is then stripped sogit fetch --unshallow/--deepencannot recover pre-cutoff objects. - Verify (fail-closed) — the build is rejected unless it is genuinely isolated:
no alternates,
.git/shallowpresent, no remotes. On any failure the shallow is removed and the launch is refused — nyia never falls back to the full.git. - Mount — the shallow
.gitis layered over the container's project.git(-v <shallow>/.git:<project>/.git:rw). The working tree stays the real project; only history is truncated. - In-container backstop — the entrypoint asserts the mounted
.gitis shallow and alternate-free when a cutoff is active, and refuses to run otherwise.
The shallow lives on disk at .nyiakeeper/git-shallow/ (gitignored, persisted — a
crash mid-session loses nothing).
The oldest retained commit must already be secret-free¶
git stores each commit's full tree, so a commit still references the secret blob as long as the file existed at that commit. The precise rule is: the secret leaks if any retained commit's tree still contains it — equivalently, the oldest retained commit must already be secret-free.
So a cutoff date is not enough on its own: if the secret was removed in commit R,
a cutoff that still retains an intermediate commit between the secret's
introduction and R will leak (that commit's tree holds the secret). Cut at or after
R so the oldest retained commit is the cleanup commit (or later) — not merely "some
date after the secret was added." When in doubt, choose a cutoff that excludes every
commit up to and including the last one that still contained the secret.
The assistant's commits: review, reconcile, recover¶
Because the container writes to the shallow .git, the assistant's new commits land
in .nyiakeeper/git-shallow/, not your real repo. They attach cleanly (SHAs match),
but importing them is always an explicit, reviewed action — never automatic.
| Command | What it does |
|---|---|
nyia git-history status |
Show the cutoff, the shallow, and any pending commits. |
nyia git-history reconcile |
Review (default, read-only): list pending commits + git diff vs your real tip. No writes. |
nyia git-history reconcile --apply |
Import pending commits into refs/nyia-shallow/<branch> (namespaced, non-forced). Your real branches are untouched; integrate with git merge/rebase/cherry-pick. |
nyia git-history reconcile --discard |
Drop the protected session. The real repository is never modified. |
nyia git-history rebuild |
Rebuild the shallow — only when nothing is pending (a rebuild over pending commits would destroy un-reconciled work). |
After each session, if pending commits exist, nyia prints a summary + the reconcile command. It does not modify your real repo.
Crash recovery (non-blocking)¶
If nyia or the assistant died mid-session, the shallow .git is intact on disk. On the
next launch nyia detects pending commits, warns, reuses the existing shallow
(new commits stack on top — it does not rebuild over your work), and continues.
The only thing ever blocked is a destructive rebuild, never the launch itself.
Non-fast-forward (real tip advanced on the host)¶
If you committed on the host after the shallow was built, --apply still imports the
assistant's commits under refs/nyia-shallow/<branch> without error. The two lines of
history diverge; you integrate them deliberately (merge/rebase/cherry-pick).
Scope & precedence¶
git_history_cutoff is a standard nyia config key, so it follows normal precedence
(CLI override → project → global → team → default). Set it per project, globally, or
per assistant like any other key. Leave it unset (the default) for today's behavior:
full history, no shallow, no reconcile machinery engaged.
Workspaces (multiple repos)¶
In workspace mode the cutoff is per repo — each member has its own history, its
own un-rotated-secret risk, and its own git_history_cutoff config.
- Which repos are considered: the workspace root (only if it is itself a git
repo — it is not listed in
workspace.conf) plus the git member repos fromworkspace.conf. Non-git RO members have no.gitand are skipped. - Per-repo opt-in: set
git_history_cutoffinside (or targeting) each repo. Repos without a cutoff mount their full.gitas usual. Heterogeneous workspaces (some protected, some not) are the normal case. - Read-protection vs reconcile: the shallow mount (read-protection) applies to any git repo with a cutoff, including RO git members. Reconcile only applies to RW repos — RO repos take no AI commits, so there is nothing to reconcile.
- Aggregated commands:
nyia git-history statusprints a per-repo table (cutoff, protected?, pending count).nyia git-history reconcileiterates the RW repos (review by default); restrict withnyia git-history reconcile --repo <path>.--apply/--discardact per repo.nyia git-history rebuildrebuilds per repo (each still refuses over pending work).- Per-repo fail-closed: if a protected repo's shallow can't be built, the workspace launch is refused (that repo cannot be mounted safely) — it is never silently mounted with full history.
- Backstop note: the in-container entrypoint assertion validates the root repo's
.gitonly. Members rely on the host-side fail-closed build (and the per-repo refusal).
Safety guarantees¶
- Fail-closed: a cutoff that cannot be built/verified refuses the launch.
- No auto-merge / no auto-push: reconcile is always explicit; imports are namespaced and non-forced; the real repo is only ever changed by your git commands.
- No data loss: the shallow is persisted; rebuild refuses to run over pending work.
- Out of scope: remote/network handling (covered by the egress policy feature).