Install
$ agentstack add skill-cyn-zhang-job-radar-job-radar ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
JobRadar
A configurable job hunting assistant. All user preferences are read from config.yaml. No hardcoded values in this file — everything is a variable.
Step 0 — Always Load Config First
Locate config.yaml by checking these paths in order — use the first one found:
./config.yaml— current working directory (project-local config takes priority)~/.claude/skills/job-radar/config.yaml— global fallback
Extract:
{name} ← hunter.name
{email} ← hunter.email
{university} ← hunter.university (string, optional)
{majors} ← hunter.majors (list, optional)
{graduation_year} ← hunter.graduation_year (integer, optional)
{level} ← hunter.level
{roles} ← hunter.roles (list)
{eligible_majors} ← hunter.eligible_majors (list, may be empty)
{locations} ← hunter.locations (list)
{industry} ← hunter.industry (list — one or more values)
{work_type} ← hunter.work_type (internship | graduate | contract | permanent | any)
{sources} ← hunter.sources (map of enabled/disabled)
{target_companies} ← hunter.target_companies (list, may be empty)
{exclude_companies} ← hunter.exclude_companies (list, may be empty)
{base_path} ← hunter.base_path
{cv_path} ← hunter.cv_path
{cv_format} ← hunter.cv_format
{cl_format} ← hunter.cover_letter_format
{digest_time} ← hunter.digest_time
{digest_channel} ← hunter.digest_channel
If config.yaml is not found, ask: > "I couldn't find config.yaml. Would you like me to create one? Just tell me your name, target roles, locations, and career level."
Step 0.1 — Validate Config
After loading, validate before proceeding. Check every rule below. If any rule fails, stop and output the full error block — do not attempt to run the requested module.
Required fields (must be present and non-empty):
hunter.name— non-empty stringhunter.email— non-empty string, contains@hunter.level— must be one of:intern | graduate | junior | mid | senior | lead | manager | director | vp | executivehunter.roles— non-empty list (at least 1 entry)hunter.locations— non-empty list (at least 1 entry)hunter.industry— non-empty list; each entry must be one of:tech | finance | accounting | consulting | healthcare | government | education | energy | retail | legal | marketing | hr | construction | manufacturing | anyhunter.work_type— must be one of:internship | graduate | contract | permanent | anyhunter.base_path— non-empty string, should end with/
Optional but typed (validate if present):
hunter.university— if present, must be a stringhunter.majors— if present, must be a list of stringshunter.graduation_year— if present and not null, must be a 4-digit integer between 1980 and 2100hunter.sources— if present, must be a map; each value must betrueorfalsehunter.digest_time— if present, must matchHH:MMformat (24h)hunter.digest_channel— if present, must be one of:gmail | slack | nonehunter.cv_format— if present, must be one of:docx | pdfhunter.cover_letter_format— if present, must be one of:docx | pdf
Error output format:
❌ config.yaml has errors — fix these before running JobRadar:
• hunter.level: "senoir" is not a valid level. Must be one of: intern | graduate | junior | mid | senior | lead | manager | director | vp | executive
• hunter.industry: "fintech" is not a valid industry. Must be one of: tech | finance | ...
• hunter.roles: is empty — add at least one target role
Run /job-setup to fix your config interactively, or edit config.yaml directly.
If validation passes, proceed silently — no success message.
Startup Greeting
Step 1 — Load status data (silent):
- Read
{base_path}tracker.jsonif it exists - Compute:
{active}= entries with status in (watchlist, applied, oa, interview, final_round, offer) - Compute:
{closing_soon}= entries wheredeadlineis within 7 days and status not in (rejected, withdrawn, ghosted, accepted) - Compute:
{interviews}= entries with status in (interview, final_round) - Read
seen.jsonif it exists → get{seen_count}= length of hashes array - Check
scans/folder → find most recent scan file → extract date as{last_scan}
Step 2 — Render greeting:
🎯 JobRadar — {name}
{if active > 0}
📊 {active} active application{s} {if interviews > 0}· {interviews} at interview stage{end}
{if closing_soon entries exist}
⚠️ Closing soon: {closing_soon entries as "Company — Role (deadline)"}
{end}
Last scan: {last_scan date or "never"}
{else}
No applications tracked yet — run /job-scan to find your first roles.
{end}
Here's what I can do:
🔍 /job-scan — search all job boards + company career pages
🧠 /job-eval — honest fit score, ATS keywords, Bottom Line verdict
📄 /job-cv — tailor CV to a specific JD
🧩 /job-gaps — fixable gaps + hard gap interview scripts
✉️ /job-cover — cover letter, 4 paragraphs, role-specific
🎤 /job-prep — STAR behavioural + technical prep + mock interview
📝 /job-oa — OA prep: coding, video interview, psychometric, case study
📬 /job-digest — send Gmail digest with today's top picks
📊 /job-track — add or update an application
🗂️ /job-status — full dashboard of all applications
🤝 /job-network — alumni map + personalised outreach drafts
✉️ /job-followup — draft follow-up or thank-you email
⚙️ /job-setup — update config (roles, locations, level, companies)
What's first?
Conciseness rule: if {active} = 0 and {last_scan} = "never", skip the stats block entirely and go straight to the command list. Don't pad with zeros.
File Structure
All files saved under {base_path}:
{base_path}
├── tracker.json ← application tracker (source of truth)
├── tracker.md ← auto-generated view (do not edit)
├── networking/
│ └── alumni_map.json ← alumni networking data (gitignored)
├── scans/
│ └── Jobs_YYYY-MM-DD.md ← daily scan results
└── {company}/
└── {job_title}/
├── jd.md ← raw JD (saved immediately on paste)
├── jd_analysis.md ← parsed JD + ATS keywords
├── coverage_map.md ← coverage table + bottom line verdict
├── gaps_and_improvements.md ← fixable gaps + hard gap scripts
├── recruiter_review_and_suggestions.md ← recruiter-eye view
├── CV_{Company}_{Role}_{date}.{cv_format}
└── CoverLetter_{Company}_{Role}_{date}.{cl_format}
Naming rules:
{company}and{job_title}= lowercase, underscores{date}= YYYY-MM-DD- Save
jd.mdimmediately on any JD paste — before analysis - Use
mkdir -pbefore writing any file - Always confirm the saved path after each write
- If filesystem unavailable: save to
/mnt/user-data/outputs/and state intended path
Quick Reference
| What you want | Say... | |---|---| | Job Scan | "scan today's roles" / "find jobs" / "search roles" / "run daily scan" | | Evaluate a JD | paste a job description, or "evaluate this JD" / "analyse this role" / "am I a good fit?" | | Tailor CV | "tailor my CV for [role]" / "customise my resume" | | Coverage Map + Gap Analysis | "run coverage map" / "gap analysis" / "show my gaps for [role @ company]" | | Cover Letter | "write a cover letter" / "cover letter for [role]" | | Interview Prep | "interview prep for [role]" / "mock interview" / "behavioural questions" / "coding assessment prep" | | Daily Digest | "send digest" / "daily jobs" / "email me today's roles" | | Track Application | "track application" / "I applied to [company]" / "update status to interview" / "show my applications" | | Application Status | "show my applications" / "what have I applied to" / "application summary" | | Update Config | "update my config" / "add [city] to locations" / "change my level to graduate" / "add [company] to target companies" / "exclude [company]" |
Auto-chain: paste a JD → analysis → offers coverage map → offers CV tailoring → offers cover letter.
Slash Commands (Claude Code)
| Command | Action | |---------|--------| | /job-scan | Daily job scan → saves scans/Jobs_YYYY-MM-DD.md | | /job-digest | Scan + Gmail draft to inbox | | /job-eval | Evaluate a JD (paste or link after the command) | | /job-gaps | Coverage map + gap analysis + recruiter review | | /job-cv | Tailor CV for a role | | /job-cover | Write a cover letter | | /job-prep | Interview prep + mock interview | | /job-oa | OA prep — coding, video, psychometric, case study | | /job-track | Add or update an application in the tracker | | /job-status | Summary of all active applications | | /job-network | Alumni map + personalised outreach drafts |
Workflow Overview
| User says... | Module | |---|---| | "Scan jobs" / "find roles" / "search today" | → Module 1: Job Scan | | Pastes JD / "evaluate this" / "analyse this role" | → Module 2 → offer Module 5 | | "Tailor my CV" / "customise my resume" | → Module 3 | | "Cover letter" | → Module 4 | | "Interview prep" / "mock interview" | → Module 6 | | "Send digest" / "daily jobs" | → Module 7 | | "Update my config" / "change my roles" | → Module 8: Config Update | | "Track application" / "I applied to X" / "update status" | → Module 9: Application Tracker | | "Show my applications" / "application summary" | → Module 10: Application Status | | "Find alumni" / "networking" / "who from my school works at X" | → Module 11: Alumni Networking |
Chain naturally: JD paste → auto Module 2 → offer Module 5 → offer Module 3 → offer Module 4.
Module 1: Job Scan
Step 0 — Stale Scan Check
Before searching, check if scans/Jobs_{today}.md already exists.
If it does: > "A scan for today ({YYYY-MM-DD}) already exists. Re-run and overwrite, append as a new run, or view existing results?"
- View: display the existing file, stop.
- Overwrite: proceed, save to
scans/Jobs_{YYYY-MM-DD}.md(replaces existing). - New run: save to
scans/Jobs_{YYYY-MM-DD}_{HH-MM}.mdusing current time (e.g.Jobs_2026-05-26_14-32.md). Preserves the original.
Step 0.5 — Load Dedup Index
Load seen.json from the project root.
If file doesn't exist, initialise in memory (do not write yet):
{ "last_updated": "{today}", "hashes": [] }
Fingerprint format — one string per listing, built at search time:
- Primary (when URL is available):
{source_key}|{url}— e.g.seek|https://www.seek.com.au/job/78291047 - Fallback (no URL):
{source_key}|{company-slug}|{role-slug}— e.g.linkedin|atlassian|swe-intern-2026
Source keys: seek, linkedin, gradconnection, indeed, company, aus_internship_finder
Slugs: lowercase, hyphens only, drop punctuation. "SWE Intern 2026/27" → swe-intern-2026.
Store the loaded hash set in memory for Step 2 filtering.
Step 1 — Build Search Queries
For each role in {roles} and each location in {locations}, build queries dynamically.
Work type filter: append {work_type} to all queries unless work_type = "any".
internship→ add "intern" OR "internship"graduate→ add "graduate" OR "grad program"contract→ add "contract" OR "fixed-term"permanent→ add "full-time" OR "permanent"
Per enabled source in {sources}:
# Seek (if sources.seek = true)
site:seek.com.au "{role}" "{work_type}" {location}
# LinkedIn (if sources.linkedin = true)
site:linkedin.com/jobs "{role}" "{work_type}" {location}
# GradConnection (if sources.gradconnection = true)
site:au.gradconnection.com "{role}" internship Australia
# Aus Internship Finder (if sources.aus_internship_finder = true)
# Company discovery only — NOT a source of truth for timing or deadlines.
# This is a single-page app — NOT indexed, do NOT use site: search.
# Use raw data to find which companies run programs:
# https://raw.githubusercontent.com/YangS1718/aus-internship-finder/main/asset.json
# For actual deadlines and open/close dates: search Seek, GradConnection, LinkedIn,
# or each company's career page directly — those are always more current.
# Search pattern: "{company_name}" careers internship {year}
# Indeed (if sources.indeed = true)
site:indeed.com.au "{role}" "{work_type}" {location}
# Company sites (if sources.company_sites = true and target_companies not empty)
"{company}" careers "{role}" {location} ← for each company in {target_companies}
Browse fallback: Some job boards (Seek, LinkedIn) are JavaScript-rendered and may return incomplete results via WebSearch/WebFetch. If a source returns 0 results or clearly incomplete data:
- If gstack
/browseis available: use it to load the page directly and extract listings - Otherwise: retry with a more specific search query, note the issue in Scanner Notes
Eligibility filter (if {eligible_majors} not empty): Include roles mentioning any keyword in {eligible_majors}. Skip roles requiring unrelated degrees only.
If {eligible_majors} is empty: no eligibility filtering — show all results.
Exclude filter (if {exclude_companies} not empty): Remove any listing where the company matches an entry in {exclude_companies}. Do this silently — don't list excluded results.
Step 2 — Score and Deduplicate
For each listing found across all sources:
2a — Build fingerprint using the format defined in Step 0.5.
2b — Dedup check:
- If the fingerprint exists in the loaded hash set → mark listing as
[seen], exclude from Results table. - If not in hash set → mark as
[new], include in Results table. - Track all
[new]fingerprints in a separate list for Step 4.
2c — Score each [new] listing:
- Role title matches
{roles}→ base score - Location matches
{locations}→ weight - Eligibility matches
{eligible_majors}→ weight (skip if empty) - Industry alignment with
{industry}→ bonus - Tech stack/skill overlap with CV → bonus (if CV loaded)
2d — Report dedup stats in the Scan Summary table: {X} new | {Y} already seen (hidden) — so user knows what was filtered.
Step 3 — Present Results
Use this exact structure every run — do not add extra sections, urgency boxes, or duplicate tables:
# {industry joined by " / "} Jobs — {YYYY-MM-DD}
Level: {level} | Work type: {work_type} | Locations: {locations joined by " | "}
Roles: {roles joined by " | "}
## Scan Summary
| Source | Status | Notes |
|--------|--------|-------|
| {source} | ✅ OK / ⚠️ Partial / ❌ Failed | {notes} |
Total found: {X} | New: {N} | Already seen: {S} (hidden) | Actionable (open now): {Y} | Not yet open: {Z} | Closed: {C}
---
## Results
| # | Company | Location | Role | Match | App Opens | App Closes | Program Dates | Source | Link |
|---|---------|----------|------|-------|-----------|------------|---------------|--------|------|
| 1 | {company} | {location} | {role} | {X}% | {date/—} | {date/🔴/⚠️/🟢} | {start–end} | {platform} | [Apply/View](...) |
...
---
## 🔥 Top Picks
1. **{role} @ {company}** — {why it's a strong fit, 1 sentence}. {call to action: "Apply today." / "Set a reminder for {date}."}
---
## ⚠️ Scanner Notes
- {source}: {issue — blocked / 0 results / timeout / estimated deadline}
- Scan time: ~{X} min
Key rules:
- Closed listings stay in the main Results table (with 🔴 Closed {date}) — do NOT move them to a separate section
- No extra "This Week" / "Act Now" / "Urgent" boxes — urgency is conveyed by 🔴 in the table and ordering
- Top Picks: one sentence why + one call to action. No paragraph descriptions.
Column definitions:
- App Opens — when applications open; use month/year or "Rolling" if continuous
- **App Clos
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: cyn-zhang
- Source: cyn-zhang/job-radar
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.