* ci: add supply-chain guards - permission allowlist, gitignore rules, manifest checks, pinned actions
This template's threat model is unusual: it ships pre-approved Claude Code
permissions (.claude/settings.json) and CLI code that every fork user
executes via those permissions. A plausible-looking PR could therefore
ship risk to every forker: widen a permission to Bash(*), weaken the
personal-data gitignore rules, or smuggle code execution into bun install
via a lifecycle script. Nothing checked for these mechanically.
New job security-guards runs tools/security_guards.py (stdlib only):
- settings.json: every permissions.allow entry must be in an exact,
in-repo allowlist. The guard makes permission changes loud, not
impossible - a PR that intentionally widens permissions must update
the allowlist in the same diff, so the widening is explicit and
reviewable
- .gitignore: the personal-data rules (tracker, documents/**, cv/main_*,
salary data, seen_jobs) must all still be present - the mirror image
of the placeholder-integrity job
- .agents/**/package.json: no lifecycle scripts (preinstall/install/
postinstall/prepare/prepack) and no trustedDependencies, which would
execute arbitrary code during bun install on users' machines
New job dependency-review (PRs only): actions/dependency-review-action
flags newly introduced vulnerable or malicious dependencies,
fail-on-severity high.
Workflow hardening: explicit top-level permissions: contents: read
(least-privilege token), and all actions pinned to commit SHAs resolved
from the same major tags already in use (checkout v4, setup-python v5,
setup-bun v2), with the tag recorded in a comment.
Honest limit, recorded in the workflow header: a PR can edit this
workflow itself, so these guards catch accidents and casual attempts,
not a determined author. Branch protection with required checks and
human review of workflow/settings diffs remain the real backstop.
Verified locally: positive run passes; injecting Bash(*) into
settings.json, deleting the tracker gitignore rule, and adding a
postinstall script each fail the guard with the intended message, and
reverting restores a clean pass.
* ci: scope dependency-review to upstream PRs - forks lack Dependency graph by default
Verified on a fork: the action fails with 'Dependency review is not
supported on this repository' until Dependency graph is manually enabled,
and forks don't inherit it. Guarded with the same
github.repository == upstream condition the other upstream-only jobs use.
With the graph enabled the action passes, so the config itself is sound.
* ci: probe Dependency graph before dependency-review - warn and pass when unavailable
The upstream PR run showed Dependency graph is disabled on the upstream
repo too (the action hard-fails: 'Dependency review is not supported on
this repository'), not just on forks. Only the repo owner can enable it,
so a hard red X here is friction, not signal.
The job now probes the dependency-graph SBOM endpoint with the workflow
token first: HTTP 200 runs the real review; anything else emits a
::warning:: naming the setting to flip (Settings -> Advanced Security ->
Dependency graph) and passes. Same graceful-skip pattern the workflow
uses for optional tools - the check self-activates the moment the graph
is enabled, no workflow change needed.
Codifies what has so far lived in PR closing comments: the universal-
template rule, merge/decline criteria with precedent links, the
feature-complete command bar, the empirical-verification standard,
and the fork path via /add-portal and the community forks discussion.
GitHub auto-surfaces CONTRIBUTING.md on new PRs and issues; README
gets a one-line pointer for browsers.
Co-authored-by: Claude Fable 5 <noreply@anthropic.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.
Every PR to this repo is currently verified by hand. This adds the checks
a machine can do:
- latex-smoke: compiles cv/main_example.tex (lualatex) and the new
cover_letters/cover_example.tex (xelatex) in the texlive/texlive
container, failing on any LaTeX error. Exact page-count assertions
(CV=2, cover letter=1) run on the upstream repo only
- lint (tools/lint_skills.py, also runnable locally): every SKILL.md has
parseable YAML frontmatter with name+description (frontmatter breakage
happened before - 37a0eed), allowed-tools 'bun run <path>' targets
exist, command files start with a '# /<name>' title, settings.json is
valid JSON with a permissions.allow list
- cli-typecheck: bun install + tsc --noEmit for all five portal CLIs
(matrix, fail-fast off)
- placeholder-integrity (upstream only): tracked template files still
carry their placeholder tokens, catching accidental personal-data
commits before they land
Fork-friendly by design: /setup personalizes CLAUDE.md, the skill files,
and main_example.tex in forks, so placeholder checks and exact page
counts are guarded with github.repository == upstream; compile success
and lint run everywhere. Live CLI smoke tests are deliberately excluded:
network-flaky, and linkedin-search is personal-use-only per its own ToS
warning - CI-automated requests would violate it. CLIs are typechecked
instead.
The cover letter previously had no tracked example (cover_*.tex is
gitignored), so cover_example.tex is new: a placeholder letter following
the documented 06 structure, demonstrating the correct itemize-outside-
lettercontent pattern. It doubles as the structural reference /apply
Step 2 looks for on fresh clones, which until now matched nothing. The
gitignore exception is ordered after Cover_*.tex because case-insensitive
filesystems match that pattern against cover_example.tex too.
Writing it surfaced a latent bug in the documented template itself:
06-cover-letter-templates.md's structure ends with \closing{Kind
regards,\} - but cover.cls appends its own \, and the doubled break
produces '! LaTeX Error: There's no line here to end.' on every compile
(nonstopmode swallows it, so it went unnoticed). Fixed in 06 and noted
in the example.
07-interview-prep.md holds STAR examples, tough-question patterns,
questions to ask, and a roleplay protocol - and the job-application-
assistant skill names interview preparation as its Step 4 - but no
command operationalizes any of it. /apply ends at 'files ready', and the
moment an interview is scheduled, nothing picks the thread back up.
/interview does, drawing on the archive /outcome now maintains.
/apply optimizes what the company reads; /interview optimizes what the
company hears. The bridge is consistency: the interviewer read the
submitted CV and cover letter, so prep must match what they claim.
How it works:
- Loads the application context from documents/applications/
<company>_<role>/: the exact posting, the submitted drafts, and
outcome.md's stage + recorded feedback (stage-N feedback is the
highest-priority input for stage-N+1 prep). Graceful fallbacks for
applications that predate /outcome, stating plainly what is missing
- Runs the Company Research Checklist that 04-job-evaluation.md defines
but nothing executes, plus interviewer-angle notes from public
professional info only. Every company claim is independently verified
before it enters the pack - same rule as cover-letter claims
- Builds a stage-specific prep pack: likely questions (earlier-stage
feedback first, then fit-evaluation gaps with honest bridge answers
per 07's you-don't-have-X pattern, then posting requirements, then
stage type), STAR mapping via 07's Use-for tags with new drafts
grounded strictly in profile facts, a consistency brief of the
submitted documents' probeable claims, customized tough questions,
4-6 questions to ask, logistics tips
- Saves the pack to documents/applications/<company>_<role>/
interview_prep_<stage>.md - one per stage, kept as history,
gitignored with the rest of the archive
- Offers a mock interview following 07's Roleplay Guidelines verbatim,
with feedback calibrated against the behavioral profile (same
voice-consistency rule the /apply reviewer applies)
- Never edits framework or profile files, except appending
user-approved STAR examples to 07 on explicit request
Integration: /outcome suggests /interview when an interview stage is
recorded; documents/README.md documents the interview_prep_<stage>.md
files (and that /setup ignores them); README commands list and file
tree.
/setup Path A already mines documents/applications/<company>_<role>/
(job_posting.md, submitted drafts, outcome.md) to calibrate
04-job-evaluation.md and surface STAR candidates - but nothing in the
workflow systematically writes those folders, so the calibration machinery
only runs for users who hand-maintain the archive. /outcome closes the
loop: it writes the data /setup reads.
How it works:
- Identifies the application from job_search_tracker.csv (by argument, or
by listing open applications); applications made outside the workflow
get a new tracker row
- Records progress updates (interview stages, offers) and resolutions
using the exact status enum documents/README.md documents, plus one
additive value: in_progress, for open applications between updates.
/setup's calibration only draws conclusions from final statuses
- Archives the submitted cv_draft.tex / cover_letter.tex (copy, never
move; existing archived files are never overwritten - the archive is
what was actually submitted) and fetches job_posting.md from the
tracker's source URL while it is still alive; a dead URL gets a
user-pasted copy or an explicit unavailable stub, never a
reconstruction
- Updates the tracker row's status and notes; never restructures the CSV
- After 3+ resolved outcomes (or a repeating pattern), points the user
back to /setup Path A - /outcome writes data, /setup interprets it,
and this command never edits framework or profile files itself
- Idempotent: re-running appends stages and dated notes, never
duplicates folders, rows, or history
Also aligns the outcome.md status enum across docs: setup.md Step A3
listed hired/rejected/no_response/interview_only while documents/README.md
already had offer_declined; both now carry the full enum including
in_progress. documents/applications/** and the tracker are already
gitignored, so all recorded data stays personal.
Docs: README (commands list, file tree), documents/README.md (/outcome
cross-reference and in_progress semantics), one-line handoff at the end
of /apply Step 6.
/scrape finds and dedupes postings; /apply evaluates one at a time in
depth. Nothing connects the two ends: after a scrape returns 20 jobs, the
user eyeballs a table to decide where to spend /apply effort. /rank is the
bridge: batch-score every new posting against the fit framework and return
a ranked shortlist.
How it works:
- Selects jobs with status "new" from job_scraper/seen_jobs.json (--all
re-ranks everything unapplied; a focus argument filters), excluding
anything already in job_search_tracker.csv
- Dispatches parallel general-purpose agents (~5 jobs each) that WebFetch
each posting and score the five dimensions from 04-job-evaluation.md.
The rubric (skill match areas, career goals, deal-breakers) is passed
inline per the same token-efficiency rules /apply uses; agents score
only from actually fetched content and mark dead postings expired,
never guessing from a title
- Triage depth by design: posting text vs. profile only - no company
research, no salary lookups. /apply's Step 1 evaluation stays
authoritative and always re-runs on handoff
- Aggregates with the framework's 30/25/15/30 weighting and verdict
bands; location deal-breakers veto regardless of score; deadlines
within 7 days get urgency flags and win ties
- Updates seen_jobs.json additively (status "ranked"/"expired" plus
rank_score/rank_verdict/rank_date) so /scrape dedup keeps working;
the tracker is read-only. Re-running is idempotent
Integration: job-scraper SKILL.md documents the new status values and
suggests /rank after large scrape batches; README (commands list, file
tree, quick-start step 4).
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>
A general-purpose, field-agnostic job-search skill built on LinkedIn's public
jobs-guest endpoints. Works for any market out of the box — location is an
explicit required flag (no country default). Zero runtime dependencies (bun only);
search + detail commands.
Includes a personal-use / Terms-of-Service note (automated access is against
LinkedIn's ToS — keep volume low, non-commercial).
(Pairs with the .gitignore fix in #21, which lets skills under .agents/ be tracked.)
Co-authored-by: Akhil Tripathi <kodabear@Akhils-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* refactor: fold /setup_docs into /setup as third onboarding path
- Auto-detect documents/ at /setup Step 0; offer three paths (documents
folder, single CV import, interview).
- Inline document-driven merge logic into setup.md as Path A: read-before-
write, additive vs. conflicting bucket merge with per-conflict prompts,
inference labeling for behavioral and style files, STAR-stub generation
instead of fabrication.
- Step 3 substeps for skill files skip when Path A populated them; non-
skill substeps (CLAUDE.md, cv/main_example.tex, search-queries.md)
always run. Path C interview preserved verbatim. /setup --section <name>
update flow preserved.
- Delete .claude/commands/setup_docs.md.
- Update /reset prompts to point only at /setup. Update documents/README.md
to reference /setup throughout. Update README.md to drop the standalone
/setup_docs section and add upskill/ to the skills tree.
- Remove the trailing comma left in .claude/settings.local.json after the
PR #6 revert (was technically invalid JSON).
Single onboarding entry point. documents/ folder convention unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(README): finish readme alignment for the /setup fold
- Update onboarding-paths tip to reflect three paths (was "Either"
before /setup_docs landed in PR #6).
- Add documents/ and upskill/ entries to the file tree, both relied
on by the new /setup Path A and by /upskill respectively.
- Add a brief "Other commands" section briefly describing /expand and
/upskill so users discover them without having to read individual
command files. /reset stays in the existing "Starting over"
subsection, now linked from the new section.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds four contributions from @Michael-Bach:
- /setup_docs - document-driven profile population from a documents/ folder (CV, LinkedIn export, diplomas, references, past applications). Idempotent merge with explicit additive vs. conflicting buckets and per-conflict prompts.
- /reset - typed-RESET confirmation gate for clearing profile data and/or documents folder contents.
- /expand - additive competency enrichment from documents and public URLs already in the profile (GitHub repos, portfolio sites), with web-searched syllabus lookups for named courses and certifications.
- /upskill - skill-gap analysis vs tracked jobs (or single URL), produces a prioritized heatmap and learning plan with year-tagged WebSearch queries.
Also adds the documents/ folder convention with README and gitignore entries for personal output files.
Closes#6
The original bullet ("Token-efficient. Reviewer receives drafts inline
rather than re-reading files. Verification runs once, at the end.") is
technically true for the reviewer-dispatch path but invites readers to
infer that the overall workflow uses fewer tokens than master. That is
not actually defensible: the new Step 5 (compile-and-inspect) spends
tokens on Read'ing both PDFs and on layout-fix iteration cycles, which
typically offsets the savings from inline draft passing and scoped
reviewer reads. End-to-end the workflow is roughly token-neutral, with
the variance dominated by how many layout fixes the CV needs.
Reframes the bullet as "Token-efficient reviewer dispatch" (which is
accurate and verifiable from the prompt structure) and adds an explicit
note that Step 5 spends some of those savings on PDF rendering. The
trade is real - tokens for fewer broken PDFs reaching the user - and
worth surfacing honestly rather than overclaiming.
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>