A practical, end-to-end guide. One model. Three intake modes (web research, the Claude Code CLI, or your Gmail inbox). A deterministic local scorer. A Claude-drafted outreach writer. A single-file review dashboard. Same loop you'd run for any role — here, the brief is the Founding Head Chef for eatcookjoy-uae.com.
The original guide split the work between Codex (cloud research)
and Claude Code (local engineering). We started there, then
collapsed the architecture onto a single provider once Claude's
web_search tool landed — fewer API keys, one billing
line, one set of model behaviours to calibrate against. The
deterministic scorer is the part that actually matters; it doesn't
care which model produced the candidate JSON.
The shipped artefact lives in two places on this site. The CVfast page is the product landing — generic, role-agnostic, links to setup. The EatCookJoy recruiter folder is the brief-specific instance, with sample data already scored and a working dashboard you can open right now.
Phase one is research: Claude hunts public profiles, press
mentions and platform pages via the server-side
web_search tool (or, in our inbound flow,
reads CV attachments out of Gmail). Phase two is engineering:
Claude Code, running locally, ranks the candidates with a
deterministic Python scorer, drafts personalised outreach, and
serves a single-file review dashboard. The two phases talk to
each other through the same repo folder — no message bus, no
database, just JSON files on disk.
web_search tool browses public profiles and press, returns 25 candidates as JSON. Or: Gmail inbox-scan does the same shape from inbound CVs.
Claude has two faces here: the API (with web_search
and the messages endpoint) and the Claude Code CLI running on
your laptop. They are the same model under the hood, but the
affordances differ. Use the API for scriptable, headless work.
Use the CLI when you want to watch and steer.
| Job | Claude API (web_search) | Claude Code CLI (local) |
|---|---|---|
| Search the open web for candidates | Best — server-side web_search, fits in a cron |
Also great — WebSearch + WebFetch tools, interactive |
| Read & edit files in this repo | — | Best — runs as a CLI in your terminal |
| Score candidates deterministically | — | Best — wrote score.py once, runs forever |
| Draft personalised outreach | Best — uses tone library + brief in one call | Same model, just driven from the CLI |
| Read CVs out of Gmail | Best — inbox_scan.py + the Anthropic SDK |
Possible but slower for batch |
| Build the dashboard UI | — | Best — writes HTML/JS, hot-reload as you watch |
npm i -g @anthropic-ai/claude-code # the CLI
git clone https://github.com/azizsaif1967-afk/aziz-saif-story.git
cd aziz-saif-story/eatcookjoy-recruiter
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # paste ANTHROPIC_API_KEY
eatcookjoy-recruiter/
├── briefs/eatcookjoy-uae-chef.yaml # role brief
├── candidates/claude-run-01.json # research output (sample data shipped)
├── candidates/scored.json # generated by scripts/score.py
├── outreach/tone.md # voice library
├── outreach/<safe_name>.txt # one draft per top-N candidate
├── scripts/research.py # Claude + web_search hunter
├── scripts/inbox_scan.py # Gmail (read-only) → candidate JSON
├── scripts/score.py # deterministic scorer
├── scripts/draft_outreach.py # Claude email drafter
├── scripts/dedupe.py # 90-day ATS cooldown
├── dashboard.html # single-file review UI
├── claude-research-prompt.md # prompt to paste into Claude Code CLI
├── INBOX_SCAN.md # 5-min Google OAuth walkthrough
├── Makefile, requirements.txt, .env.example, README.md
└── voice-to-hire/ # voice-note → brief experiment
gmail.readonly) — the script
cannot send, label, archive or delete anything.
gmail-credentials.json and gmail-token.json
are gitignored and never leave your laptop. CV text is sent to
the Anthropic API for field extraction; or pass --dry-run
to use a local, regex-only extractor that needs no API key.
Run this once for EatCookJoy's first chef role and you'll have the muscle memory to do it for any role at any company. The brief changes; everything downstream is the same.
| # | Step | What happens |
|---|---|---|
| 1 | Brief | YAML in briefs/. Skills, target arcs, filters, signals, red flags. |
| 2 | Research | make research — Claude + web_search returns 25 candidates as JSON. |
| 3 | Inbox scan (optional) | make inbox-scan — pulls CVs from Gmail and writes the same JSON shape. |
| 4 | Score | make score — local Python ranks 0–100. Deterministic. |
| 5 | Outreach | make outreach — drafts ~90-word email per top-10 using the tone library. |
| 6 | Review & send | make serve, open dashboard, approve, export approved.json, push to Gmail. |
Four small Python files plus one HTML dashboard. No frameworks.
Pure standard library plus anthropic,
pyyaml, and (for Gmail) the Google client + parsing
libraries. Below are the exact shapes — copy them as a starting
point and tune the weights for your role.
role: Founding Head Chef — Content & Kitchen
company: EatCookJoy UAE (eatcookjoy-uae.com)
target_arcs:
- "5–10 yrs in UAE 5★ hotel kitchens → ready for first head-chef role"
- "Chef de Cuisine at a Michelin / MENA's-50-Best venue → wants brand ownership"
- "Private chef in Dubai with UHNW client book + IG > 20k"
- "Ex-MasterChef / Top Chef MENA / South Asia running a supper-club"
- "Hotel exec sous-chef who already creates content"
filters:
location: UAE (Dubai or Abu Dhabi)
languages: [English B2+]
exclude_employers: [direct private-chef-marketplace competitors in UAE]
signals_we_love:
- "public Instagram with >5k followers and original cooking content"
- "has plated for a paying event (not just hotel banquet line)"
- "any media credit (Time Out Dubai, Gulf News, FACT, Esquire ME)"
output:
format: json
count: 25
fields: [name, current_employer, prior_arc, cuisine_strengths,
content_signal, why_fit, contact_channel, last_active, source_url]
WEIGHTS = {
"arc": 0.40, # prior career arc lines up with target_arcs
"content": 0.25, # IG / YouTube / TikTok reach (log-scaled)
"recency": 0.15, # how fresh the source is (linear decay / 365d)
"cuisine": 0.10, # covers UAE multi-cuisine palette
"geo": 0.10, # currently in UAE / GCC
}
def score(c, brief):
parts = {
"arc": arc_match(c["prior_arc"], brief["target_arcs"]),
"content": content_score(c.get("content_signal")),
"recency": recency_score(c.get("last_active")),
"cuisine": cuisine_fit(c.get("cuisine_strengths")),
"geo": geo_score(c["current_employer"],
brief["filters"]["location"]),
}
total = sum(parts[k] * WEIGHTS[k] for k in WEIGHTS)
return {**c, "score": round(total, 1),
"score_breakdown": {k: round(v, 1) for k, v in parts.items()}}
The scorer is deterministic on purpose. The candidate hunt is non-deterministic (Claude can wander). The shortlist ordering should not be — otherwise you can't tell whether tweaking the brief or tweaking the prompt is what changed the result.
SYSTEM = """You are a recruiter writing warm, specific outreach for
EatCookJoy UAE.
Rules:
- 80–100 words. No more.
- Open by referencing one concrete detail from their prior arc.
- One line on why EatCookJoy specifically — chef-led brand,
content engine, kitchen ownership.
- One soft CTA: a 20-min call this/next week.
- No buzzwords (no "synergy", "rockstar", "exciting opportunity").
- No emojis unless the candidate's own content uses them.
- Sign off with the sender line provided.
- Output the email body only — no subject line, no preamble.
"""
One file. Vanilla JS. No build step. fetch()s
candidates/scored.json and each
outreach/<name>.txt, renders a sortable
filterable table, and lets you tick Approve checkboxes. State
is in localStorage, so you can close the tab and
come back. The "Export approved (JSON)" button drops
approved.json that a separate Gmail / ATS sender
can pick up.
Open claude inside eatcookjoy-recruiter/
and paste this. The agent uses its built-in WebSearch
and WebFetch tools and writes the JSON file
directly. Better when you want to steer mid-search.
You are my recruiting research agent for EatCookJoy UAE.
Read the brief at briefs/eatcookjoy-uae-chef.yaml.
Use WebSearch + WebFetch to find 25 real, currently-active candidates
that match the career arcs in the brief. Then Write the result to
candidates/claude-run-NN.json (use the next free NN).
Hard rules:
- Only real, currently active people. No composites.
- Each candidate must cite a real source_url you actually retrieved.
- Skip anyone working at a direct competitor.
- Prefer candidates who moved roles, started a pop-up, or got press
in the last 24 months — they're warm.
When you're done, run `make score` and show me the top 5.
# EatCookJoy outreach tone
Voice: a founder, not a recruiter. Specific, warm, no padding.
Do:
- Open with one concrete detail you actually noticed (a dish, a venue,
a press credit). Specific > flattering.
- Name what's different about EatCookJoy in one line: chef-led brand,
content engine wired to bookings, real ownership of menu IP.
- Make the ask small: 20 minutes, this week or next.
- Sign off with a real name and a real reply-to.
Don't:
- "Exciting opportunity". Ever.
- "Rockstar", "ninja", "synergy", "passionate".
- Long paragraphs. If a sentence isn't pulling weight, cut it.
- Mass-merge tells (`Hi {{first_name}},`). Readers can smell them.
Targets vs a classic recruiter cycle for the same role. Numbers below are the working hypothesis going into v1 — we'll backfill the actuals as the first ten hires complete.
| Metric | Baseline (classic) | Target (v1) | Delta |
|---|---|---|---|
| Time brief → shortlist | 5–7 days | ~1 day | −85% |
| Cost per qualified candidate | USD 80–120 | USD 25–40 | −60% |
| Reply rate to outreach | 4–6% | 15–20% | +3× |
| Recruiter hours per hire | ~40 h | ~10 h | −75% |
| Hires from non-obvious arcs | ~15% | ~70% | +55 pp |
| Code lines you wrote yourself | n/a | ~0 | 100% Claude |
| Day 1 | Set up repo, install Claude Code, write the brief. | You |
| Day 2 | Run make research. Spot-check the candidate JSON. | Claude API |
| Day 3 | Have Claude Code build scorer + drafter + dashboard. | Claude Code |
| Day 4 | Review shortlist. Tune scoring weights against your read. | You |
| Day 5 | Send the first 10 emails. Log replies in a Google Sheet. | You + cron |
| Day 6 | Add dedupe.py against the ATS. Polish the tone library. | Claude Code |
| Day 7 | Lock the prompts as templates. Add the next role. | You |