Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/docs-local-guard.yml
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
7 changes: 7 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,10 @@ echo "Running typecheck..."
bun run typecheck || { echo "Typecheck failed"; exit 1; }

echo "All checks passed"

# Guard — no tracked docs.local/ (personal data; see scripts/guard-no-docslocal.sh)
if [ -f scripts/guard-no-docslocal.sh ]; then
# Must be `bash`: the guard uses `set -o pipefail`, which POSIX shells
# (dash on Ubuntu/Debian, where /bin/sh is dash) reject outright.
bash scripts/guard-no-docslocal.sh || exit 1
fi
103 changes: 0 additions & 103 deletions docs.local/convex-auth-patterns.md

This file was deleted.

65 changes: 65 additions & 0 deletions scripts/guard-no-docslocal.sh
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)"
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

if [ -z "$tracked" ]; then
echo "docs.local guard: OK — 0 tracked paths"
exit 0
Comment on lines +19 to +23

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Guard utility lacks tests 📘 Rule violation ☼ Reliability

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
## Issue description
The new `guard-no-docslocal.sh` utility lacks an automated unit test.

## Issue Context
Add a Vitest test that runs the script against temporary Git repositories and verifies both exit paths: success when `docs.local` is untracked and failure when a path beneath it is tracked.

## Fix Focus Areas
- scripts/guard-no-docslocal.sh[19-24]
- src/utils/guard-no-docslocal.test.ts[1-1]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The 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:

  1. nothing tracked → exit 0
  2. `git add -f docs.local/leaked.md` → exit 1, offending path listed
  3. untracked again → exit 0, and the file is still on disk (the untrack must not delete local data)
  4. the pre-commit hook invokes `bash`, not `sh` — regression test for the dash bug in the sibling thread

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
102 changes: 102 additions & 0 deletions src/__tests__/guard-no-docslocal.test.ts
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);
});
});
Loading