From 34b8b3f91f28fe9546150afb092224c5ee27e3a2 Mon Sep 17 00:00:00 2001 From: Mads Lorentzen <50207393+MadsLorentzen@users.noreply.github.com> Date: Wed, 19 Aug 2026 21:33:32 +0200 Subject: [PATCH] fix(onboarding): warn about public forks at the point of decision (#345) (#348) The quick start walked a new user into gh repo fork - forks of public repos are always public - and two steps later had /setup write personal data into tracked files, with the only complete warning in SETUP.md section 8, a section about pulling updates that a first-time user has no reason to open during onboarding. A real user hit exactly this (#345). The warning now sits adjacent to both fork commands (README step 1, SETUP.md section 2, both pointing at section 8's private-remote recipe), and /setup checks the origin's visibility BEFORE writing anything: a public-fork origin gets a confirm-first warning instead of a note after every file is on disk. A private origin, no origin, or a non-git directory continues silently. Reported by @basilevs with a complete reproduction and fix analysis; this implements his fixes (1) and (4). Co-authored-by: Claude Opus 5 (1M context) --- .claude/commands/setup.md | 21 +++++++- CHANGELOG.md | 10 ++++ README.md | 9 ++++ SETUP.md | 8 +++ tests/test_onboarding_privacy.py | 85 ++++++++++++++++++++++++++++++++ 5 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 tests/test_onboarding_privacy.py diff --git a/.claude/commands/setup.md b/.claude/commands/setup.md index 31a0ebe..1194ae5 100644 --- a/.claude/commands/setup.md +++ b/.claude/commands/setup.md @@ -10,7 +10,26 @@ There are three paths into setup. Step 0 picks the right one; all three converge If `$ARGUMENTS` contains `--section `, skip directly to that section in Path C for an update-only flow. Do not run the path-selection prompt below. -Otherwise, before greeting the user, scan the `documents/` folder. Use Glob with `documents/**/*` and count files per subfolder (`cv/`, `linkedin/`, `diplomas/`, `references/`, `applications/`). +Otherwise, first check where this working copy would publish to — **before anything is +written, not after** (the Step 4 privacy note fires only once every file is already on +disk, which is too late to inform the decision). Run `git remote get-url origin`; if the +command fails (no remote, or not a git checkout), skip this check silently. If there is +a GitHub `origin`, check it with `gh repo view --json visibility,isFork` +when `gh` is available. If the origin is a **public fork** of the template — or its +visibility cannot be determined — warn now and wait: + +> **Heads-up before we start:** your `origin` points at ``, which is a +> public GitHub fork. This setup writes your personal data (name, contact details, +> employment history, salary expectations) into **tracked** files, and anything you +> commit *and push* to that fork is visible to anyone. Two safe options: keep your +> profile commits local and never push them, or push to a **private** repository +> instead — SETUP.md section 8 has the two-minute private-remote recipe. Want to +> continue with the setup? + +Wait for the user's confirmation before showing the path prompt. A private origin, no +origin, or a non-fork remote needs no warning — continue silently. + +Then, before greeting the user, scan the `documents/` folder. Use Glob with `documents/**/*` and count files per subfolder (`cv/`, `linkedin/`, `diplomas/`, `references/`, `applications/`). Then welcome the user with a single message that lists three paths. The wording changes based on what was found. diff --git a/CHANGELOG.md b/CHANGELOG.md index bddfc9f..26980c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -135,6 +135,16 @@ per-file diff commands. ### Fixed +- **Onboarding warns about public forks at the point of decision** (#345) - the quick + start walked a new user into `gh repo fork` (forks of public repos are always public) + and two steps later had `/setup` write personal data into tracked files, with the only + complete warning sitting in SETUP.md section 8 - a section about pulling updates that a + first-time user has no reason to open. A real user hit exactly this. The warning now + sits adjacent to both fork commands (README step 1, SETUP.md section 2), and `/setup` + checks the origin's visibility **before** writing anything: a public-fork origin gets a + confirm-first warning instead of a note after every file is already on disk. Reported + by @basilevs with a complete reproduction and fix analysis. Pinned by the new + `tests/test_onboarding_privacy.py`. - **`jobindex-search detail` rewritten against jobindex's current markup** - every selector the old parser used is gone from live pages, so on 4 of 5 live postings it returned CSS-comment text as the deadline (`"K \t\t... */"`), an external ATS URL as diff --git a/README.md b/README.md index 564bb5b..c1dbd62 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,15 @@ gh repo fork MadsLorentzen/ai-job-search --clone cd ai-job-search ``` +> [!IMPORTANT] +> **A fork of this repo is always public** — GitHub does not allow private forks of +> public repositories — and `/setup` (step 3 below) writes your personal data (name, +> contact details, employment history, salary expectations) into **tracked** files. +> If this copy is for your own job search rather than for contributing changes back, +> use a **private repository** with this repo as `upstream` instead — the two-minute +> recipe is in [SETUP.md section 8](SETUP.md#8-pulling-upstream-updates-into-your-fork), +> and every update workflow works identically. Fork only to contribute. + ### 2. Install job search tools PowerShell: diff --git a/SETUP.md b/SETUP.md index 8a0e682..a4d7c77 100644 --- a/SETUP.md +++ b/SETUP.md @@ -160,6 +160,14 @@ cd ai-job-search Or manually: fork on GitHub, then clone your fork. +> **Before you go further: forks are public.** GitHub cannot make a fork of a public +> repository private, and `/setup` (section 6) writes your personal data into **tracked** +> files — pushing those commits to a fork publishes them. If this copy is for your own +> job search rather than for contributing, prefer a **private repository** with this repo +> as `upstream`: see section 8, step 1 for the exact commands and why committing your +> personalization there is still the right move. Everything else in this guide works +> identically either way. + ## 3. Install job search CLI dependencies Run these from the repository root. diff --git a/tests/test_onboarding_privacy.py b/tests/test_onboarding_privacy.py new file mode 100644 index 0000000..8456022 --- /dev/null +++ b/tests/test_onboarding_privacy.py @@ -0,0 +1,85 @@ +"""Guards for the onboarding privacy warnings (issue #345). + +The README's quick start walks a new user into creating a public fork +(forks of public repos cannot be private) and then has /setup write +personal data into tracked files, with the only complete warning sitting +in SETUP.md section 8 - a section about pulling updates, downstream of +the decision it should inform. A real user hit exactly this. These tests +pin that the warning lives at the point of decision (adjacent to both +fork commands) and that /setup checks the origin's visibility BEFORE +writing anything, not in its closing notes. +""" +import re +import unittest +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +README = REPO / "README.md" +SETUP_GUIDE = REPO / "SETUP.md" +SETUP_COMMAND = REPO / ".claude" / "commands" / "setup.md" + + +def section(text: str, heading: str) -> str: + """Body of a markdown section up to the next heading of the same level.""" + level = heading.split(" ")[0] + pattern = re.compile( + rf"^{re.escape(heading)}\n(.*?)(?=^{level} |\Z)", re.MULTILINE | re.DOTALL + ) + match = pattern.search(text) + return match.group(1) if match else "" + + +class TestForkWarningsAtTheDecisionPoint(unittest.TestCase): + def assert_warns(self, body: str, where: str): + self.assertRegex( + body, + re.compile(r"public", re.IGNORECASE), + f"{where}'s fork section must say the fork will be public", + ) + self.assertIn( + "personal data", + body, + f"{where}'s fork section must say /setup writes personal data into tracked files", + ) + self.assertRegex( + body, + re.compile(r"section 8|§8|#8-pulling", re.IGNORECASE), + f"{where}'s fork section must point at SETUP.md section 8's private-remote recipe", + ) + + def test_readme_quick_start_warns_next_to_the_fork_command(self): + body = section(README.read_text(encoding="utf-8"), "### 1. Fork and clone") + self.assertIn("gh repo fork", body, "sanity: the fork command lives in this section") + self.assert_warns(body, "README") + + def test_setup_guide_warns_next_to_the_fork_command(self): + body = section(SETUP_GUIDE.read_text(encoding="utf-8"), "## 2. Fork and clone") + self.assertIn("gh repo fork", body, "sanity: the fork command lives in this section") + self.assert_warns(body, "SETUP.md") + + +class TestSetupChecksOriginBeforeWriting(unittest.TestCase): + def test_preflight_exists_and_precedes_profile_generation(self): + text = SETUP_COMMAND.read_text(encoding="utf-8") + self.assertIn( + "git remote get-url origin", + text, + "/setup must check where the working copy would publish to", + ) + preflight_at = text.index("git remote get-url origin") + writes_at = text.index("## Step 3: Generate Profile Files") + self.assertLess( + preflight_at, + writes_at, + "the origin check must run before any profile file is written - the " + "existing Step 4 note fires after everything is already on disk", + ) + self.assertIn( + "public", + text[max(0, preflight_at - 2000) : preflight_at + 2000].lower(), + "the preflight must be about public visibility, not just remote presence", + ) + + +if __name__ == "__main__": + unittest.main()