You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Guard the private/ convention: verify private/ is its own standalone repo before anything reaches GitHub
Problem
The hooks already block staged ^private/ paths on public branches (pre-commit private-content check), but nothing verifies the structural convention that makes private content safe by construction: private/ being its own standalone git repo (dz private-init). Today a project can sit for months with private/ as a plain directory -- one .gitignore edit away, on any machine, from private content becoming stageable in the parent repo. A real project was found this week with 626 files in a non-repo private/ and no .gitignore inside it; the guard rail existed only as a convention in people's heads.
Why repo-ness is the load-bearing property:
git will not recurse into a nested repo. If private/.git exists, git add private/ (or add -A after a broken ignore rule) stages a single gitlink entry, not the tree. The failure mode collapses from "hundreds of private files committed" to "one inert SHA committed" -- and the existing private-content regex still catches that path.
A standalone repo means the private workspace is versioned locally (crash recovery, history for design docs), and the commit workflow's private-sync step actually functions.
The convention only works if it holds on every machine that clones the project -- exactly the thing a hook can check and a human forgets.
Proposed solution
Two layers, both cheap:
1. Local hook check (pre-commit, runs with the existing checks):
# private/ exists but is not its own repo -> warn (default) or block (configurable)if [ -d"$REPO_ROOT/private" ] && [ !-e"$REPO_ROOT/private/.git" ];then
warn "private/ is not a standalone git repo -- run 'dz private-init --adopt'"fi# private/ content tracked in the PARENT index -> always block (this is live leakage)if git ls-files --error-unmatch -- 'private/'>/dev/null 2>&1|| \
[ -n"$(git ls-files -- 'private/')" ];then
block "private/ paths are tracked in the parent repo index"fi
The repo-ness check needs no dazzlecmd at hook time: [ -e private/.git ] is the whole test (works for both a .git dir and a worktree/submodule-style .git file). dz private-init is the recommended remedy, not a hook dependency.
The tracked-in-parent-index case is a hard block with no config: that is not a convention drift, it is private content already in history's staging area.
2. CI backstop (GitHub Actions snippet shipped in this repo's docs/templates):
- name: No private/ content in treerun: | if git ls-files | grep -q '^private/'; then echo '::error::private/ paths are tracked in this repository'; exit 1 fi
The hook is the "before we get to GitHub" gate; the CI job is the server-side proof that no machine's misconfigured clone ever slipped private content into a pushed commit. CI cannot check repo-ness of a directory that (correctly) never gets pushed -- so the two layers check complementary things: local checks the convention, CI checks the outcome.
--no-untracked-style noise is zero: both checks are single test/ls-files calls, consistent with the hook's one-process-per-check performance posture.
The nested-repo gitlink protection is defense-in-depth on top of gitignore, not a replacement: the recommended state is both (/private/ ignored in the parent AND private/.git present AND a .gitignore inside private/ for bulk archival content like revisions/).
dz private-init --status already reports exactly the states this check distinguishes; the hook's warning text should quote its remedy commands verbatim so the fix is one paste away.
Projects that legitimately have no private/ directory: both checks are no-ops (guarded by [ -d private ]).
Acceptance criteria
Pre-commit warns (naming the remedy: dz private-init --adopt) when private/ exists without private/.git
.repokit-limitsprivate_repo = required escalates that warning to a block; absent config means warn
Pre-commit hard-blocks when git ls-files -- private/ is non-empty in the parent repo, regardless of config
Both checks are no-ops when no private/ directory exists
A documented CI snippet (README or workflow template) fails the build if any private/ path is tracked in the pushed tree
Hook README documents the two-layer model: local = convention, CI = outcome
Guard the private/ convention: verify private/ is its own standalone repo before anything reaches GitHub
Problem
The hooks already block staged
^private/paths on public branches (pre-commit private-content check), but nothing verifies the structural convention that makes private content safe by construction:private/being its own standalone git repo (dz private-init). Today a project can sit for months withprivate/as a plain directory -- one.gitignoreedit away, on any machine, from private content becoming stageable in the parent repo. A real project was found this week with 626 files in a non-repoprivate/and no.gitignoreinside it; the guard rail existed only as a convention in people's heads.Why repo-ness is the load-bearing property:
private/.gitexists,git add private/(oradd -Aafter a broken ignore rule) stages a single gitlink entry, not the tree. The failure mode collapses from "hundreds of private files committed" to "one inert SHA committed" -- and the existing private-content regex still catches that path.Proposed solution
Two layers, both cheap:
1. Local hook check (pre-commit, runs with the existing checks):
[ -e private/.git ]is the whole test (works for both a.gitdir and a worktree/submodule-style.gitfile).dz private-initis the recommended remedy, not a hook dependency..repokit-limitsline (the config file proposed in Pre-commit guard gaps: 500KB warn tier, lower block threshold, first-submission binary detection #6) escalates it:private_repo = required.2. CI backstop (GitHub Actions snippet shipped in this repo's docs/templates):
The hook is the "before we get to GitHub" gate; the CI job is the server-side proof that no machine's misconfigured clone ever slipped private content into a pushed commit. CI cannot check repo-ness of a directory that (correctly) never gets pushed -- so the two layers check complementary things: local checks the convention, CI checks the outcome.
Design considerations
--no-untracked-style noise is zero: both checks are singletest/ls-filescalls, consistent with the hook's one-process-per-check performance posture./private/ignored in the parent ANDprivate/.gitpresent AND a.gitignoreinsideprivate/for bulk archival content likerevisions/).dz private-init --statusalready reports exactly the states this check distinguishes; the hook's warning text should quote its remedy commands verbatim so the fix is one paste away.private/directory: both checks are no-ops (guarded by[ -d private ]).Acceptance criteria
dz private-init --adopt) whenprivate/exists withoutprivate/.git.repokit-limitsprivate_repo = requiredescalates that warning to a block; absent config means warngit ls-files -- private/is non-empty in the parent repo, regardless of configprivate/directory existsprivate/path is tracked in the pushed treeRelated issues
.repokit-limitsconfig mechanism and the warn-by-default philosophy