Skip to content

Guard the private/ convention: verify private/ is a standalone repo (dz private-init) before anything reaches GitHub #7

Description

@djdarcy

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.
  • Default warn for the missing-repo case (legacy projects shouldn't start failing overnight); a .repokit-limits line (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.
  • 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 tree
  run: |
    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.

Design considerations

  • Warn-first mirrors Pre-commit guard gaps: 500KB warn tier, lower block threshold, first-submission binary detection #6's philosophy: the hook teaches the convention loudly without breaking legacy repos; enforcement is per-repo opt-in via config, not hook edits (which leak upstream -- hooks/pre-push on main carries AMDead's project-specific modifications (leaked back upstream) #5).
  • --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-limits private_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

Related issues

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions