-
Notifications
You must be signed in to change notification settings - Fork 0
chore(security): untrack docs.local and guard against re-adding it #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| name: docs.local guard | ||
|
|
||
| on: | ||
| push: | ||
| branches: [master] | ||
| pull_request: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| docs-local-guard: | ||
| name: No tracked docs.local | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| persist-credentials: false | ||
| - name: Guard against tracked docs.local | ||
| run: bash scripts/guard-no-docslocal.sh |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| #!/usr/bin/env bash | ||
| # Guard: no docs.local/ path may ever be TRACKED by git. | ||
| # | ||
| # Why this exists (2026-09-01): | ||
| # docs.local/ held verbatim speech-to-text dictation transcripts and desktop | ||
| # screenshots containing personal data. Seven such blobs reached the public | ||
| # repo and forced a full history rewrite plus a repo rebuild. | ||
| # | ||
| # .gitignore did NOT prevent it. `git add -f` overrides .gitignore, and once a | ||
| # path is tracked, .gitignore is ignored for that path forever after. | ||
| # This guard is the part that actually holds. | ||
| # | ||
| # Exit 0 = clean. Exit 1 = tracked docs.local paths found. | ||
| # | ||
| # Used by: .githooks/pre-push and .github/workflows/ci.yml | ||
|
|
||
| set -uo pipefail | ||
|
|
||
| tracked="$(git ls-files -- 'docs.local' 'docs.local/**' 2>/dev/null || true)" | ||
|
|
||
| if [ -z "$tracked" ]; then | ||
| echo "docs.local guard: OK — 0 tracked paths" | ||
| exit 0 | ||
|
Comment on lines
+19
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1. Guard utility lacks tests The new guard-no-docslocal.sh utility has no corresponding automated test covering its clean and blocked outcomes. Regressions in the repository-protection logic could therefore pass unnoticed. Agent Prompt
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 7d27208 — added `src/tests/guard-no-docslocal.test.ts` (Vitest, throwaway git repos via `mkdtemp`), covering both branches you cited plus two more:
4 tests, green; full suite 203 passed, tsc clean. |
||
| fi | ||
|
|
||
| count="$(printf '%s\n' "$tracked" | grep -c . || true)" | ||
|
|
||
| cat <<BANNER | ||
|
|
||
| BLOCKED — $count docs.local path(s) are TRACKED by git. | ||
|
|
||
| docs.local/ is local-only scratch. It has previously contained dictation | ||
| transcripts and screenshots with personal data. Tracked files here end up | ||
| published the moment this repo is public. | ||
|
|
||
| Tracked paths: | ||
| BANNER | ||
|
|
||
| printf '%s\n' "$tracked" | sed 's/^/ /' | ||
|
|
||
| cat <<'BANNER' | ||
|
|
||
| To fix (this removes them from git, NOT from your disk): | ||
|
|
||
| git rm -r --cached docs.local | ||
| git commit -m "chore: untrack docs.local" | ||
|
|
||
| WARNING — read before you merge that commit: | ||
| `git rm --cached` keeps the files only in the worktree that ran it. | ||
| EVERY other checkout that pulls the merge gets those paths DELETED from | ||
| its working tree. Back up first, outside git: | ||
|
|
||
| cp -a docs.local ~/backups/$(basename "$PWD")-docs.local-$(date +%Y-%m-%d)/ | ||
|
|
||
| If a checkout already lost them, restore with: | ||
|
|
||
| git checkout <merge-sha>^1 -- docs.local/ && git reset HEAD docs.local/ | ||
|
|
||
| After merging, check EVERY checkout (`git worktree list`), not just this one. | ||
|
|
||
| Do not bypass with --no-verify. | ||
|
|
||
| BANNER | ||
|
|
||
| exit 1 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| import { describe, it, expect, beforeAll, afterAll } from "vitest"; | ||
| import { execFileSync, spawnSync } from "node:child_process"; | ||
| import { | ||
| mkdtempSync, | ||
| rmSync, | ||
| mkdirSync, | ||
| writeFileSync, | ||
| readFileSync, | ||
| } from "node:fs"; | ||
| import { tmpdir } from "node:os"; | ||
| import { join, resolve } from "node:path"; | ||
|
|
||
| const GUARD = resolve(process.cwd(), "scripts/guard-no-docslocal.sh"); | ||
| const HOOK = resolve(process.cwd(), ".husky/pre-commit"); | ||
|
|
||
| /** Build a throwaway git repo with the guard script copied in. */ | ||
| function makeRepo(): string { | ||
| const dir = mkdtempSync(join(tmpdir(), "guard-docslocal-")); | ||
| const git = (...args: string[]) => | ||
| execFileSync("git", args, { cwd: dir, stdio: "pipe" }); | ||
|
|
||
| git("init", "-q"); | ||
| git("config", "user.email", "test@example.com"); | ||
| git("config", "user.name", "test"); | ||
|
|
||
| mkdirSync(join(dir, "scripts"), { recursive: true }); | ||
| mkdirSync(join(dir, "docs.local"), { recursive: true }); | ||
| writeFileSync( | ||
| join(dir, "scripts/guard-no-docslocal.sh"), | ||
| readFileSync(GUARD), | ||
| ); | ||
| writeFileSync(join(dir, ".gitignore"), "docs.local/\n"); | ||
| writeFileSync(join(dir, "README.md"), "test\n"); | ||
| git("add", "README.md", ".gitignore", "scripts/guard-no-docslocal.sh"); | ||
| git("commit", "-qm", "init"); | ||
|
|
||
| return dir; | ||
| } | ||
|
|
||
| function runGuard(dir: string, shell = "bash") { | ||
| return spawnSync(shell, ["scripts/guard-no-docslocal.sh"], { | ||
| cwd: dir, | ||
| encoding: "utf8", | ||
| }); | ||
| } | ||
|
|
||
| describe("guard-no-docslocal.sh", () => { | ||
| let dir: string; | ||
|
|
||
| beforeAll(() => { | ||
| dir = makeRepo(); | ||
| }); | ||
|
|
||
| afterAll(() => { | ||
| rmSync(dir, { recursive: true, force: true }); | ||
| }); | ||
|
|
||
| it("exits 0 when no docs.local path is tracked", () => { | ||
| // docs.local exists on disk and is gitignored, but nothing is tracked. | ||
| writeFileSync(join(dir, "docs.local/scratch.md"), "local only\n"); | ||
|
|
||
| const res = runGuard(dir); | ||
|
|
||
| expect(res.status).toBe(0); | ||
| expect(res.stdout).toContain("0 tracked paths"); | ||
| }); | ||
|
|
||
| it("exits 1 when a docs.local path is force-added past .gitignore", () => { | ||
| // `git add -f` is exactly how .gitignore gets defeated in practice. | ||
| writeFileSync(join(dir, "docs.local/leaked.md"), "sensitive\n"); | ||
| execFileSync("git", ["add", "-f", "docs.local/leaked.md"], { cwd: dir }); | ||
|
|
||
| const res = runGuard(dir); | ||
|
|
||
| expect(res.status).toBe(1); | ||
| expect(res.stdout).toContain("docs.local/leaked.md"); | ||
| }); | ||
|
|
||
| it("returns to exit 0 once the path is untracked, leaving the file on disk", () => { | ||
| execFileSync("git", ["rm", "-q", "--cached", "docs.local/leaked.md"], { | ||
| cwd: dir, | ||
| }); | ||
|
|
||
| const res = runGuard(dir); | ||
|
|
||
| expect(res.status).toBe(0); | ||
| // The whole point: untracking must not delete the user's local file. | ||
| expect(readFileSync(join(dir, "docs.local/leaked.md"), "utf8")).toBe( | ||
| "sensitive\n", | ||
| ); | ||
| }); | ||
|
|
||
| it("is invoked with bash, not sh, by the pre-commit hook", () => { | ||
| // Regression test: the guard uses `set -o pipefail`, which dash rejects. | ||
| // On Ubuntu/Debian /bin/sh is dash, so `sh scripts/guard-no-docslocal.sh` | ||
| // aborted with "Illegal option -o pipefail" and rejected EVERY commit. | ||
| const hook = readFileSync(HOOK, "utf8"); | ||
|
|
||
| expect(hook).toMatch(/bash scripts\/guard-no-docslocal\.sh/); | ||
| expect(hook).not.toMatch(/(^|[^a-z])sh scripts\/guard-no-docslocal\.sh/m); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.