resumasher
Smart agent skills utilize user project details to customize resumes, generate cover letters, and build interview preparation packages tailored for specific job positions. It outputs ATS-friendly PDFs, helping students and job seekers apply for jobs efficiently.
git clone https://github.com/earino/resumasher.gitBefore / After Comparison
1 组Each time you apply for a new job, you need to manually modify your resume to match job requirements, write personalized cover letters, and prepare for interviews from scratch. This process is time-consuming and laborious, easily leads to missing key information, and makes it difficult to ensure that your materials highly match the position, resulting in low job application efficiency.
With resumasher, you only need to provide a job description, and it can automatically extract evidence from project files, customize your resume, generate a cover letter, and create an interview preparation package. This significantly saves time, ensures that application materials are highly relevant to the position, and increases the success rate of job applications.
resumasher
Invoked as /resumasher <job-source> from inside the student's resume folder.
<job-source> is one of:
- A path to a file containing the job description (
job.md,jd.txt). - A URL to a job posting.
- Literal text pasted after the command.
Optional flags: --style eu|us (override config default), --photo <path> or --no-photo (override config default).
Prerequisites
The skill requires Python 3.10+ with these packages (see requirements.txt):
reportlab, pdfminer.six, chardet, nbconvert.
Workflow
Follow these phases in order. Every deterministic helper is available as a Python module under scripts/, and every LLM phase dispatches via the Task tool with subagent_type="general-purpose".
Setup: resolve paths in EVERY Bash tool call
⚠️ CRITICAL: Claude Code's Bash tool runs every command in a fresh shell. Variables set in one Bash tool call do NOT persist to the next. If you set SKILL_ROOT in one Bash call and reference "$SKILL_ROOT/..." in the next, $SKILL_ROOT will be empty and the command will fail with permission denied or file not found.
Every single Bash tool call that touches resumasher's code MUST begin with the path prologue below. It's short. Just paste it at the top of every command. Don't try to "remember" values from a prior call — they're gone.
The prologue (paste at the top of every Bash tool call):
SKILL_ROOT=""
NEEDS_INSTALL=""
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
for c in \
"$HOME/.claude/skills/resumasher" \
"$PWD/.claude/skills/resumasher" \
"$REPO_ROOT/.claude/skills/resumasher" \
"$HOME/.codex/skills/resumasher" \
"$PWD/.codex/skills/resumasher" \
"$REPO_ROOT/.codex/skills/resumasher" \
"$HOME/.gemini/skills/resumasher" \
"$PWD/.gemini/skills/resumasher" \
"$REPO_ROOT/.gemini/skills/resumasher" \
"$HOME/.opencode/skills/resumasher" \
"$PWD/.opencode/skills/resumasher" \
"$REPO_ROOT/.opencode/skills/resumasher"; do
[ -n "$c" ] || continue
[ -f "$c/SKILL.md" ] || continue
if [ -x "$c/.venv/bin/python" ] || [ -x "$c/.venv/Scripts/python.exe" ]; then
SKILL_ROOT="$c"; break
else
NEEDS_INSTALL="$c"
fi
done
if [ -z "$SKILL_ROOT" ]; then
if [ -n "$NEEDS_INSTALL" ]; then
echo "ERROR: resumasher found at $NEEDS_INSTALL but its Python venv is missing." >&2
echo "This means install.sh was never run after git clone. Fix:" >&2
echo " bash $NEEDS_INSTALL/install.sh" >&2
else
echo "ERROR: resumasher is not installed. See https://github.com/earino/resumasher#install" >&2
fi
exit 1
fi
RS="$SKILL_ROOT/bin/resumasher-exec"
TEL="$SKILL_ROOT/bin/resumasher-telemetry-log"
STUDENT_CWD="$PWD"
This sets:
SKILL_ROOT— absolute path to the installed skill (user-scope OR project-scope).RS— absolute path to thebin/resumasher-execwrapper that auto-locates the venv Python and the right script.TEL— absolute path tobin/resumasher-telemetry-log, the no-op-when-tier-off event logger called at 8 pipeline boundaries below.STUDENT_CWD— where the student is working (their resume folder, NOT the skill dir).
Telemetry identifiers you (the orchestrator) substitute literally: $MODEL and $HOST. Many "$TEL" calls below pass --model "$MODEL" and --host "$HOST". These are NOT shell variables the prologue sets — they're strings you substitute with literals before executing the command.
$MODEL: your own model identifier. Examples:claude-opus-4-7,claude-sonnet-4-6,gpt-5-codex,gpt-5-mini,gemini-2.5-pro,gemini-2.5-flash. You know what you are. If you genuinely don't, omit--model; null is better than fabricated.$HOST: which AI CLI you're running in. Exactly one ofclaude_code,codex_cli,gemini_cli, oropencode_cli. You know this — it's literally the CLI that loaded this SKILL.md. If omitted, the log script falls back to env-var sniffing and then to"unknown", which is what we want to avoid.
Both are self-reported because bash can't reliably detect them across host CLIs (Codex, for instance, doesn't set a discoverable env var).
The check distinguishes three failure modes:
- SKILL_ROOT set, success — everything good, proceed.
- NEEDS_INSTALL set, SKILL_ROOT empty — skill was cloned but
install.shwas never run. Error message names the exact command to fix it. This is the "future Claude cloned the repo and forgot the install step" case. - Both empty — skill isn't installed at all. Point the user at the README install section.
Every helper call in this document looks like:
"$RS" orchestration <subcommand> [args...] # e.g., discover-resume, mine-context, company-slug
"$RS" render_pdf --input ... --output ... # PDF rendering
"$RS" github_mine <username> # GitHub profile mine
The $RS wrapper handles three things for you: locating SKILL_ROOT by following its own path, execing the venv Python (not system Python — those dependencies aren't installed there), and picking the right script file. Do not run python -m scripts.orchestration or python scripts/orchestration.py directly; use $RS instead.
Run scratch files go in $STUDENT_CWD/.resumasher/run/ — NOT /tmp/. That directory is:
- Already gitignored (the top-level
.resumasher/entry). - Scoped to the student's working folder, not system-global.
- Wiped at the start of each run so prior scratch can't leak.
Create it once per run, near the top:
RUN_DIR="$STUDENT_CWD/.resumasher/run"
rm -rf "$RUN_DIR"
mkdir -p "$RUN_DIR"
Then every intermediate — resume text, folder context, sub-agent outputs — writes into $RUN_DIR/, not /tmp/.
Interactive prompt pattern (cross-host)
This skill runs on Claude Code, Codex CLI, Gemini CLI, and OpenCode. Each host has a different tool name but the same contract: present 2+ real options, let the student type free text in an "Other" field. The tools are:
- Claude Code:
AskUserQuestion - Codex CLI:
request_user_input(NOTask_user_question— that's an unshipped enhancement request) - Gemini CLI:
ask_user - OpenCode:
question
Wherever this document says "use the question tool" or names AskUserQuestion, use whichever tool your host provides. Reference them with backticks — models match fenced tool names more reliably than bare prose.
⚠️ All four tools require a MINIMUM of 2 real options. "Other" is auto-added and does NOT count toward the minimum. Supplying only 1 option crashes with InputValidationError: Too small: expected array to have >=2 items (Claude) or "request_user_input requires non-empty options for every question" (Codex). Gemini and OpenCode are similarly strict. This is the #1 first-run-setup bug to avoid.
Your job when collecting a free-text value is to avoid TWO separate mistakes:
- Passing only 1 explicit option (API error, nothing happens).
- Designing a middleman flow where round 1 asks "will you provide a value?" and round 2 actually collects it (API works, but doubles the prompts).
Both are avoidable with the right 2-option + Other shape.
✅ Correct pattern A — when a default value exists (e.g., you extracted name / email / phone / linkedin / location from a resume.pdf):
Question: "Phone number for the resume?"
A) Use the value from your resume: "+43 664 1234567"
B) Skip — don't include phone on the tailored resume
Other: paste a different phone number
Two real options (A = accept default, B = skip), plus Other for the student to override. One round, collects the value immediately.
✅ Correct pattern B — when no default exists (e.g., GitHub username, photo path — the PDF doesn't contain these):
Question: "Do you have a GitHub? We can leverage it for this."
A) I have one — paste the username/URL in Other below
B) Skip — leave blank; set github_prompted=true so we don't re-ask
Other: paste your GitHub username or profile URL
Two real options (A = I'll provide a value, use the Other field on this screen, B = skip permanently), plus Other for the actual value. Student picks "Other" in practice (since that's where the input is) — A exists purely to satisfy the minimum-2 constraint AND to give a visible hint that there IS an input field.
❌ Wrong pattern 1 — 1 real option (API error):
Question: "Phone number?"
A) Skip
Other: paste your phone ← InputValidationError, too few options
❌ Wrong pattern 2 — middleman (2 rounds):
Round 1: "Phone number?"
A) Skip
B) I'll enter it ← Student picks B
Round 2: "Type your phone number in Other field"
A) (forced placeholder)
Other: paste real value ← Actual value arrives here
Doubles the prompts; the student could have pasted in round 1's Other directly.
Apply pattern A or B to every free-text collection: name, email, phone, location, LinkedIn, photo path, GitHub username.
No interactive tool available — hard-fail fallback
If none of the three question tools is available (e.g., codex exec non-interactive mode, a CI script run, or a host that doesn't yet ship any of them), do NOT guess values from context. Silent inference produced wrong configs for ambiguous inputs in v0.1 — students got run-time decisions they didn't make.
Instead:
-
Stop before Phase 1.
-
Write a skeleton
.resumasher/config.jsonin$STUDENT_CWDwith every required field set to the sentinel string"__ASK__". Includename,email,phone,linkedin,location,default_style,include_photo,photo_path,photo_position,github_username, andgithub_prompted: false. -
Print exactly this message to stdout, then exit with code 2:
resumasher needs answers to its setup questions but this host does not support interactive prompts. Edit .resumasher/config.json, replace every "__ASK__" value with your real answer (use "" to skip optional fields like linkedin/photo_path), then re-run the skill.
This halt-and-resume path is the ONLY acceptable fallback. Never infer name, email, GitHub username, or style from resume content or JD location.
Sub-agent prompt pattern (cross-host)
Every LLM sub-agent resumasher dispatches (folder-miner, fit-analyst, company-researcher, tailor, cover-letter, interview-coach) uses a prompt built from runtime content — the student's resume, the folder summary, the JD, etc.
Do NOT build these prompts inline with string interpolation. A previous design had the orchestrator LLM substitute {resume_text} / {folder_summary} / {jd_text} tokens before dispatching. Cross-host testing revealed this is unreliable: under Gemini CLI, the fit-analyst sub-agent received a prompt with {resume_text} unfilled and produced a fit assessment that literally said "the resume section is a placeholder." Claude and Codex happened to substitute, but LLM judgment is the wrong tool for a mechanical string operation.
Instead, use build-prompt:
PROMPT=$("$RS" orchestration build-prompt --kind <kind> --cwd "$STUDENT_CWD" [--out-dir "$OUT_DIR"] [--company "$COMPANY"])
build-prompt reads the appropriate files from $RUN_DIR/ / .resumasher/cache.txt / $OUT_DIR/, substitutes them into the kind's template (defined in scripts/prompts.py), and emits the fully-rendered prompt to stdout. No LLM-side substitution, no ambiguity. If a required file is missing, build-prompt exits with code 2 and a clear error naming the file and the phase that produces it.
If a prompt is too large to round-trip through a shell variable (the folder-miner prompt routinely exceeds 100KB on a real GitHub mine, and some hosts cap argv length at 128KB), stage the rendered prompt to a file inside $RUN_DIR/prompts/ — NEVER /tmp/ — then read it back when dispatching:
mkdir -p "$RUN_DIR/prompts"
"$RS" orchestration build-prompt --kind folder-miner --cwd "$STUDENT_CWD" \
> "$RUN_DIR/prompts/folder-miner.txt"
PROMPT=$(cat "$RUN_DIR/prompts/folder-miner.txt")
$RUN_DIR/prompts/ is gitignored (parent .resumasher/ is) and gets wiped at the start of every run, so prompt staging never leaks across sessions and never lands on the student's git history. /tmp/ is forbidden for prompt staging because: (1) on macOS it's world-readable to other local users until reboot, exposing the student's resume + JD + project content as plaintext PII; (2) prompt files there can outlive the run and accumulate across sessions; (3) we have no cleanup hook for /tmp paths the agent improvises. A defense-in-depth cleanup scan (Phase 9) catches and deletes any /tmp/<kind>-prompt.txt files that slip through anyway, but the SKILL.md prescription above is the first line of defense — please follow it.
Then dispatch the sub-agent with $PROMPT as the instruction text. Pass $PROMPT AS-IS — do not paraphrase, summarize, shorten, or rewrite it before dispatching. The compiled prompt has been carefully tuned per kind: it includes labeled <<<...BEGIN>>>/<<<...END>>> markers around resume, folder summary, JD, and company-research blocks; it includes prompt-injection defenses for UNTRUSTED content; it includes the exact ordering of structural instructions like "Start with a greeting H1" that downstream rendering depends on. A weak model that "improves" the prompt by handcrafting a shorter version (observed under qwen3.6-35b on OpenCode, run ses_235c — Qwen rewrote the cover-letter prompt and inverted "Start with" to "End with", causing the salutation to render as a giant H1 at the bottom of the PDF) ships broken artifacts that look superficially correct. The dispatch primitive AND the subagent_type value differ per host — use the entry that matches the CLI you're actually running in, not the first one listed. Picking the wrong subagent_type returns Unknown agent type: <X> is not a valid agent type and burns a dispatch attempt (observed under qwen3.6-35b on OpenCode, run ses_235c — the model defaulted to Claude Code's general-purpose and got rejected before self-correcting to OpenCode's general).
- Claude Code:
Tasktool withsubagent_type="general-purpose"and the prompt asdescription/prompt. - OpenCode:
tasktool (lowercase) withsubagent_type="general"(NOT"general-purpose"— that's Claude Code's value) and the prompt asdescription/prompt. Same shape as Claude Code'sTask. Note: same-message parallel dispatch works in current builds but has been historically flaky (sst/opencode#14195) — if two concurrent dispatches serialize instead of running in parallel, that's known and benign. - Gemini CLI:
@generalist(its built-in generalist sub-agent). - Codex CLI: explicitly instruct the model to spawn a sub-agent — "spawn a sub-agent with the following prompt and return its output." Without the explicit spawn request, Codex tends to run the task inline in the parent session (still produces correct output,
...
User Reviews (0)
Write a Review
No reviews yet
Statistics
User Rating
Rate this Skill