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) <noreply@anthropic.com>
This commit is contained in:
Mads Lorentzen
2026-08-19 21:33:32 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent ab5d23bad4
commit 34b8b3f91f
5 changed files with 132 additions and 1 deletions
+20 -1
View File
@@ -10,7 +10,26 @@ There are three paths into setup. Step 0 picks the right one; all three converge
If `$ARGUMENTS` contains `--section <name>`, skip directly to that section in Path C for an update-only flow. Do not run the path-selection prompt below. If `$ARGUMENTS` contains `--section <name>`, 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 <owner/repo> --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 `<owner/repo>`, 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. Then welcome the user with a single message that lists three paths. The wording changes based on what was found.
+10
View File
@@ -135,6 +135,16 @@ per-file diff commands.
### Fixed ### 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 - **`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 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 returned CSS-comment text as the deadline (`"K \t\t... */"`), an external ATS URL as
+9
View File
@@ -78,6 +78,15 @@ gh repo fork MadsLorentzen/ai-job-search --clone
cd ai-job-search 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 ### 2. Install job search tools
PowerShell: PowerShell:
+8
View File
@@ -160,6 +160,14 @@ cd ai-job-search
Or manually: fork on GitHub, then clone your fork. 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 ## 3. Install job search CLI dependencies
Run these from the repository root. Run these from the repository root.
+85
View File
@@ -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()