* security(skills): drop bare Bash and the portal CLI glob from skill grants
A skill's allowed-tools pre-approves the listed tools for the turn that
invokes it, even in an untrusted folder. job-application-assistant granted
bare Bash, Edit and Write; job-scraper granted bun run on any
.agents/skills/*/cli/src/cli.ts. Every Bash command the skills document is
already allowlisted per command in .claude/settings.json, so the bare and
glob grants bought nothing but exposure. The per-portal grants stay in
settings.json, which is where a portal added by /add-portal registers too
(Step 5 now says so).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* security(settings): deny exfil-shaped shell and credential reads, always ask on permission-defining files
Deny rules block in every permission mode including bypassPermissions and
override any allow rule or skill grant; ask rules force a prompt even when
a skill's allowed-tools would pre-approve the call. settings.json now
ships both: deny for shell that uploads or opens a remote shell (curl
data/form/upload flags, wget post, scp, sftp, ssh, nc) and for reads of
.env and ~/.ssh, ~/.aws, ~/.gnupg; ask for the files whose content is the
permission surface (settings, skill frontmatter, agents, CI, this guard)
and for git push. security_guards.py requires each rule to stay present,
the same way it requires the personal-data gitignore rules. Prefix rules
are tripwires, not walls; SECURITY.md will say so.
Also allowlists tools/robots_check.py and the upcoming tools/fetch_page.py
so the 403 retry no longer needs a curl grant.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* security(guards): hold skill and command allowed-tools to a reviewed allowlist
The guard allowlisted settings.json permissions but never read skill
frontmatter, so the narrow settings allowlist sat next to a bare Bash
grant it could not see. Check 4 parses every .claude/skills/*/SKILL.md,
.agents/skills/*/SKILL.md and .claude/commands/*.md frontmatter with a
stdlib parser (scalar, folded, and list forms) and holds each
allowed-tools entry to ALLOWED_SKILL_TOOLS; a portal skill may
additionally grant exactly its own cli/src/cli.ts, matched by path so no
portal name is hardcoded. Bare Bash and mid-pattern wildcards get their
own message. On master's frontmatter this check reports two failures.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(tools): add GET-only fetch_page.py for the browser-header retry
09-web-research.md documented the 403 retry as a curl command with browser
headers plus an inline python3 -c tag-stripper, which meant the workflow
wanted a Bash(curl:*) grant - and curl with a free tail is an upload
channel. fetch_page.py is the same retry as one command with a fixed GET
argv (URL after --, bounded to 45 s / 5 redirects / 10 MB), http(s) only,
robots_check.gate() enforced with no bypass flag, --out refused inside
the repo, and --text for the stripped page. urllib fallback when curl is
absent. The settings.json grant names the script.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(security): describe the permission surface as a table; route the 403 retry through fetch_page.py
SECURITY.md now lists every shipped grant with its location, lifetime
and guard, and says plainly what the permission layer does not do: no
sandbox on native Windows, prefix rules are tripwires, WebFetch is
unrestricted by domain, subagents run with the parent's tools (next
change), and Edit/Write stay granted because tailoring a CV is editing
files. 09-web-research.md's retry section is the one fetch_page.py
command (framework_version 1.1.3 -> 1.1.4); apply, rank and scrape point
at it instead of curl. SETUP.md and the README tools tree follow.
CHANGELOG gains a Security section under [Unreleased].
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* security(review): ask on command frontmatter, portal skills grant only their CLI, bound the urllib fallback
Findings from a fresh-context review of the branch:
- A command's frontmatter is a grant too (check 4 already parses it), so
Edit(.claude/commands/*.md) joins the required ask rules; without it a
steered session could write `allowed-tools: Bash` into apply.md with no
prompt and the next /apply would be pre-approved for bare Bash.
- SECURITY.md promised a portal skill may grant "its own CLI and nothing
else"; the guard now enforces exactly that instead of own-CLI plus the
core allowlist. All six shipped portal skills already comply.
- fetch_page.py's urllib fallback now matches curl's 5-redirect cap and
applies the 10 MB cap after decompression, so the docstring's bounds
hold on both paths.
- Frontmatter parser: a BOM no longer reads as "missing frontmatter",
YAML flow lists split correctly, and a non-UTF-8 file fails with a
message instead of a traceback. Module docstring lists check 4.
- CHANGELOG no longer claims "no new prompt": compile and aux cleanup,
which the removed bare Bash covered in a skill-invoking turn, now
prompt, deliberately. "Once per session" wording corrected to per call.
- Three upload-shaped deny siblings added (curl -XPOST, curl --request
POST, wget --method=POST); /add-portal recon points at fetch_page.py.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The TinyTeX snippet exports PATH for the current session only. A new terminal
window then fails with 'command not found: lualatex' although the install worked.
Add the persisted form and a one-line check.
README and SETUP.md hardcoded the six shipped portals in four PowerShell/Bash
loops, so a portal added by /add-portal or borrowed from another fork was
skipped on a fresh clone. tools/install_portal_clis.py uses CI's discover-clis
glob, isolates per-CLI failures, and skips manifests with lifecycle scripts or
trustedDependencies.
gh repo fork --clone - SETUP.md's own fork command - sets the upstream
repo as gh's default repository, which gh uses for creating issues and
PRs. A user's own automation running gh issue create from a fork clone
therefore published personal job-search data on the upstream public
tracker. SETUP.md section 2 now includes gh repo set-default in the fork
commands with a point-of-decision warning, and .github/ISSUE_TEMPLATE/
carries the same heads-up the PR template already had for the web path.
Blank issues stay enabled.
* feat(ats): extract PDF text with pypdf before Poppler
Lead the ATS text-layer check with pypdf (BSD, optional pip install). Fall back to pdftotext -layout -enc UTF-8. No cache directory, no installer, no AGPL pymupdf. Windows users without Poppler still get a mechanical parseability check; visual review remains the last resort.
* Update verify_pdf.py
* Update apply.md
* Update verify_pdf.py
* Update verify_pdf.py
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>
Forks tracking this template face a weekly "which of these commits do I
actually care about?" question. check_upstream_updates.py answers it at the
file level (version stamps); this adds the commit-level half.
tools/upstream_triage.py walks the commits a fork is behind and splits them
into "worth reviewing" and "probably skip". Work already ported drops off on
its own via git patch-id, commits touching only files the fork removed are set
aside, and SHAs in .github/upstream-wontport.txt stay hidden. It reports and
nothing more - ready-to-run cherry-pick lines, but no merge, push, or PR,
since on a fork "applies cleanly" is not "correct".
.github/workflows/upstream-watch.yml runs it weekly into one rolling issue. It
no-ops on the upstream template (guarded, and pinned by a test) and uses only
the built-in GITHUB_TOKEN, so it can never write outside its own fork. The two
tools point at each other in their output; README, SETUP 8, and CHANGELOG
introduce them together. Tests cover patch-id matching, relevance filtering,
the won't-port list, and the workflow guard - all offline.
Co-authored-by: Angelina Lok <angelina@chattermill.io>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Section 8 told users to 'git fetch upstream' but never showed how to add that remote, so forks that cloned from their own fork could not follow the steps. Spell out the one-time remote-add command (already printed by tools/check_upstream_updates.py) at the source.
Forks of public GitHub repositories are always public, so SETUP.md
section 8's 'your fork is private working space, so commit them' invited
exactly the personal-data exposure it seemed to rule out - the observed
failure mode behind several real forks that pushed filled-in profiles to
public master. Section 8 now states the fork-is-public fact plainly and
documents the safe alternative (private repo + template as upstream
remote), and /setup's completion summary carries a matching privacy note
at the exact moment profile data first lands in tracked files.
Also backfills the CHANGELOG entry for #265.
Prompted by rasstamann's discussion #266.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses #213 (how to keep up with a fast-moving upstream) and closes the
verification loophole surfaced in the 2026-07-22 triage audit.
- Add CHANGELOG.md (Keep a Changelog + semver), with v1.0.0 as the first
tagged baseline and an Unreleased section for going forward.
- SETUP.md section 8: recommend updating to a tagged release (a vetted,
described checkpoint) over pulling raw master; fetch --tags and merge a tag.
- README: add a "Staying up to date" pointer to Releases, the CHANGELOG, and
check_upstream_updates.py.
- CONTRIBUTING.md: sharpen "Claims get verified" - a test that distinguishes
master from the fix is necessary but not sufficient; the failing input must
be one the workflow actually produces, not one the test hand-builds. Fixes
demonstrated only through a synthetic input the real code path never receives
get declined even when their test is green.
Note: the git tag / GitHub Release for v1.0.0 is intentionally left for the
maintainer to cut.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Documents the Windows Basic MiKTeX path missing from SETUP.md's minimal-TeX section: enabling silent [MPM]AutoInstall so missing-package installs don't block on a GUI prompt in non-interactive terminals, an mpm pre-install alternative matching the macOS TinyTeX package list, and a PowerShell translation of the existing bash smoke tests. Nested under the LaTeX section as a peer of the TinyTeX/BasicTeX subsection.
By @oscarbol09 (first contribution). Package names, AutoInstall config, and the PowerShell smoke block empirically verified on Windows 11 + MiKTeX.
CVs from /apply were named cv/main_<company>.tex, so a second role at the same company overwrote the first (cover letters already carried the role). Aligns CV naming to main_<company>_<role>.tex across apply, add-template, the CV template guide, CLAUDE.md, and SETUP.md; /outcome and /interview fallbacks glob main_<company>*.tex to match both legacy and new names. framework_version bumped on both touched framework files.
The freehire-search CLI is typechecked in CI but was missing from install
instructions, the file tree, and the country-agnostic portal overview.
Co-authored-by: fooSynaptic <19420328+fooSynaptic@users.noreply.github.com>
Document a user-level TinyTeX path for minimal LaTeX installs, list the template packages needed by the stock CV and cover-letter templates, and add local smoke-test commands for lualatex/xelatex.
An ATS reads the compiled PDF's embedded text layer, not the rendered page,
and LaTeX can silently produce PDFs whose text extracts as garbage: icon
glyphs where contact details should be, (cid:*) markers from fonts without
Unicode mappings, interleaved lines from multi-column layouts. This matters
more now that /add-template lets users bring arbitrary templates. The
existing Step 5 loop verifies what a human sees; this adds verification of
what a parser sees.
New Step 5d in /apply (CV only - cover letters rarely go through keyword
screening; cleanup renumbered to 5e):
- Extract the CV PDF's text layer with pdftotext -layout. pdftotext
(poppler) is an optional dependency: if missing, the mechanical check is
skipped with a warning and keyword coverage falls back to the visual PDF
read - the same graceful-skip pattern as salary_lookup.py
- Parseability checks verified against a real extraction of the stock
template: email/phone must survive as literal text (fontawesome icons
extract as harmless glyph-name noise like MOBILE-ALT/Envelope, but a
contact detail carried only by an icon or hyperlink is invisible to ATS),
no (cid:*) or replacement-character garbage, reading order matching
visual order, dates present
- Keyword coverage reuses the required/preferred list from Step 1, matched
in the posting's language, reported as covered / synonym-only /
missing-have-it / missing-gap. Honesty rule enforced: keywords the
profile genuinely supports get added to experience bullets; genuine gaps
stay visible, never stuffed
Integration: CLAUDE.md verification checklist section, ATS Parseability
guidance in 05-cv-templates.md, narrow Bash(pdftotext:*) entry in the
pre-approved permissions (keeping with the tightened scope from #27),
cv/*.txt gitignored (extraction is personal data; also deleted by the
step itself), and optional-dependency docs in README and SETUP.
The README has always invited users outside Denmark to build equivalents of
the four Danish portal CLI skills, but doing so meant reverse-engineering
.agents/skills/*/cli/ by hand. /add-portal turns that invitation into a
guided workflow:
- Interviews the user for the portal URL, skill name, market/language
(trigger phrases in the local language, like the Danish skills), and a
realistic test query
- Investigates the portal before writing code: search-URL pattern, result
structure (JSON API preferred over HTML), detail-page pattern, robots.txt
and access rules. Auth-walled portals are declined; portals with
restrictive terms get a prominent personal-use-only warning in the
generated SKILL.md (same as linkedin-search)
- Scaffolds from the canonical structure with linkedin-search as the
zero-dependency reference, enforcing the shared portal-skill contract:
search/detail commands, common flags, {meta, results} JSON shape, stderr
JSON errors, backoff on 429/5xx, chunked parsing
- Mandatory live test-run (search + detail + test suite) before registering
- Optionally wires the portal into /scrape via search-queries.md
The generator is country-agnostic; its output is market-specific and stays
in the user's fork, matching the repo policy that upstream remains a
universal template.
Docs: README (commands list, file structure, Job search tools section) and
SETUP.md (CLI install section pointer).
Users could already swap the stock moderncv/cover.cls templates, but only by
hand-editing the guidance in 05-cv-templates.md and 06-cover-letter-templates.md.
/add-template automates that:
- Interviews the user for the template's instructions: compile engine, fonts
(bundled files or system), style rules to preserve, and hard page limit
- Stores the template profile-agnostic ([PLACEHOLDER] tokens) under templates/
with a TEMPLATE.md manifest, so templates are safe to commit and share
- Runs a mandatory test compile with dummy data before registering anything
- Activates via a single managed block in 05/06, which /apply already reads,
so no changes to the /apply workflow are needed; --use default is a clean
revert to the stock templates
- --list and --use <name> manage multiple registered templates
Docs: README (commands list, file structure, LaTeX templates section) and
SETUP.md (compile section pointer).
Follow-up to #20: the linkedin-search CLI landed without being added to
the bun install instructions. Included in both lists with a note that
the install is optional (zero runtime dependencies, dev types only).
Also updated the README file tree and Bun prerequisite wording to
reflect that the toolset is no longer Denmark-only.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix: move scoped permissions to settings.json, drop curl, untrack settings.local.json
Addresses #23:
- Remove pre-approved Bash(curl:*) - no agent workflow uses curl, and a
toolkit that routinely feeds untrusted job postings to the model should
not ship a pre-approved exfiltration-capable command
- Move shared permissions to .claude/settings.json (committed by
convention) and scope them tighter: Bash(bun run:*) for the job portal
CLIs, Bash(python/python3 salary_lookup.py:*) for salary lookups
- Untrack .claude/settings.local.json - it was committed despite being
listed in .gitignore; the file stays local for personal overrides
Reported-by: @josealfonsomora
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(setup): warn existing cloners about stale settings.local.json
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Two stragglers in SETUP.md still recommended pdflatex even though the
README, CLAUDE.md, and 05-cv-templates.md all correctly say lualatex.
The compile snippet in section 7 used pdflatex, and the troubleshooting
section described the CV compile as "standard LaTeX." Both updated to
match the rest of the documentation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates README.md and SETUP.md to reflect the new /apply workflow
additions shipped in the previous commit:
- Bumps the /apply step list from 6 to 7, adding "Compile and inspect"
between Revise and Present
- Adds a "What makes this workflow different" subsection highlighting the
PDF verification loop, relevance-weighted CV cutting, drafter-reviewer
separation, and token-efficient dispatching
- Updates prerequisites note to call out lualatex (CV) and xelatex (cover
letter) explicitly, with the reason each engine is required
- Updates SETUP.md's LaTeX section to match (pdflatex -> lualatex for the
CV, with the MiKTeX fontawesome5 caveat)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>