From job-apply
Automates job applications on LinkedIn Easy Apply, Greenhouse, Ashby, Lever, Rippling, and Workday using browser automation. Extracts resume data into a reusable profile.
How this skill is triggered — by the user, by Claude, or both
Slash command
/job-apply:job-applyThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
A Claude Code skill for filling job applications on LinkedIn Easy Apply, Greenhouse, Ashby, Lever, Rippling, and Workday using browser automation.
A Claude Code skill for filling job applications on LinkedIn Easy Apply, Greenhouse, Ashby, Lever, Rippling, and Workday using browser automation.
When this skill is invoked, first check if a profile exists at ~/.claude-job-profile.json.
If NO profile exists, say:
Welcome to the Job Application Assistant! I'll help you fill out job applications on LinkedIn, Greenhouse, and Workday.
First, I need to set up your profile. This is a one-time process — your information will be saved for future applications.
Please provide the path to your resume file (PDF, DOCX, or TXT).
For example:
~/Documents/resume.pdfor/Users/you/Desktop/MyResume.pdf
Then wait for the user to provide the path before proceeding with profile extraction.
If a profile DOES exist, say:
Welcome back! Your profile is loaded from
~/.claude-job-profile.json.Provide a job URL and I'll help you apply. For example:
- LinkedIn:
https://www.linkedin.com/jobs/view/123456789- Greenhouse:
https://boards.greenhouse.io/company/jobs/123- Workday:
https://company.wd5.myworkdayjobs.com/jobs/job/123Or say "reset profile" if you want to update your information from a new resume.
Your extracted profile is stored at ~/.claude-job-profile.json for reuse across sessions. Run with --reset-profile to re-extract from resume.
This skill uses two browser automation tools together because each has capabilities the other lacks:
| Capability | Chrome MCP | Playwright MCP |
|---|---|---|
| Authenticated sessions (LinkedIn) | Yes | No — runs a separate browser with no login |
| File uploads | No — opens OS file picker that can't be controlled | Yes — setInputFiles and browser_file_upload |
| Iframe interaction | Limited — can't reliably reach iframe content | Yes — snapshots include iframe elements transparently |
| JavaScript injection | Yes — javascript_tool | Yes — browser_evaluate and browser_run_code |
| Dropdown/combobox interaction | Limited | Yes — browser_fill_form and browser_click |
Use Chrome MCP (mcp__claude-in-chrome__*) for:
Use Playwright MCP (mcp__plugin_playwright_playwright__*) for:
When a LinkedIn job uses an external application portal, the Apply button opens a new tab. Use JavaScript interception to capture that URL.
window.open (most common)// Step 1: Set up interceptor in Chrome MCP
var originalOpen = window.open;
window.open = function(url, target, features) {
document.title = 'CAPTURED:' + url;
var w = originalOpen.call(window, url, target, features);
return w;
};
// Step 2: Click the Apply button
document.querySelector('button.jobs-apply-button').click();
// Step 3: Read document.title — it will start with "CAPTURED:" followed by the URL
target="_blank" (fallback)// Remove target="_blank" so it navigates in the same tab
var link = document.querySelector('a.jobs-apply-button');
link.removeAttribute('target');
link.click();
// Read the URL from the Chrome tab after navigation
After capturing the URL, pass it to Playwright MCP via browser_navigate.
If no profile exists at ~/.claude-job-profile.json, or if the user requests a reset:
firstName, lastNameemail, phonelocation (city, state, country, zip)linkedInUrl, portfolioUrl, githubUrl (if present)workHistory[]: array of { company, title, startDate, endDate, current, description }education[]: array of { school, degree, field, startDate, endDate, gpa }skills[]: array of skill stringsresumePath: absolute path to the resume file on disk~/.claude-job-profile.json~/.claude-job-profile.jsonlinkedin.com) → use Chrome MCP to navigate (authenticated)browser_navigatebrowser_snapshotbrowser_fill_form, handle dropdowns with browser_clickbrowser_file_upload or browser_run_code using setInputFilesCharacteristics:
Approach:
read_page on each step to identify fieldsupload_image tool with resume path)Field patterns to look for:
input[name*="phone"] - Phone numberinput[type="file"] - Resume uploadselect, [role="listbox"] - Dropdown questions[role="radio"], [role="checkbox"] - Multiple choiceCharacteristics:
Approach (use Playwright MCP):
browser_navigatebrowser_snapshot to read full form — iframe content appears with f54eXX refsbrowser_fill_formbrowser_file_upload or browser_run_code with setInputFilesField patterns:
<input> and <select> elements#first_name, #last_name, #email, #phone common IDs.field-container or .field wrapping each questionCharacteristics:
Approach (use Playwright MCP):
browser_navigatebrowser_snapshot to read form structurebrowser_fill_form (name, phone, email, LinkedIn URL)browser_run_code with page.locator('#_systemfield_resume').setInputFiles('/path/to/resume.pdf') — target #_systemfield_resume specifically, not the autofill inputbrowser_snapshotCharacteristics:
company.com/careers/...?lever-source=LinkedIn)Approach (use Playwright MCP):
browser_navigatebrowser_snapshot to read form structurebrowser_fill_formbrowser_click doesn't work (overlay intercepts), use browser_evaluate with:
element.click();
element.dispatchEvent(new Event('change', {bubbles: true}));
browser_run_code to find the hidden file input and call setInputFiles:
async (page) => {
await page.locator('input[type="file"][name="resume"]').setInputFiles('/path/to/resume.pdf');
}
browser_snapshot, then submitCharacteristics:
Approach (use Playwright MCP):
browser_navigatebrowser_snapshot to see what was auto-filledbrowser_fill_formbrowser_snapshot, then submitCharacteristics:
Approach (use Playwright MCP):
browser_snapshot to read form structure on each pagebrowser_click to open, then browser_snapshot to read options, then browser_click optionbrowser_file_upload or browser_run_codeSpecial handling:
browser_snapshot → click option| Profile Field | Common Form Labels |
|---|---|
| firstName | First Name, Given Name, First |
| lastName | Last Name, Family Name, Surname, Last |
| Email, Email Address, E-mail | |
| phone | Phone, Phone Number, Mobile, Cell |
| location.city | City |
| location.state | State, Province, State/Province |
| location.zip | Zip, Postal Code, ZIP Code |
| location.country | Country |
| linkedInUrl | LinkedIn, LinkedIn URL, LinkedIn Profile |
| workHistory[0].company | Current Company, Most Recent Employer, Company |
| workHistory[0].title | Current Title, Job Title, Position, Title |
| education[0].school | School, University, College, Institution |
| education[0].degree | Degree, Degree Type |
| education[0].field | Major, Field of Study, Concentration |
Use read_page with filter: "interactive" to see all form fields
Use find with natural language: "email input field", "submit button"
Use form_input for standard inputs and selects:
- ref: element reference from read_page
- value: the value to set
Use computer with action: "left_click" for custom dropdowns:
1. Click to open dropdown
2. read_page to see options
3. Click the correct option
Use find to locate file input: "resume upload field"
Use form_input or computer to trigger file picker
Note: May need to use upload_image tool for some implementations
After filling current page:
1. find "next button" or "continue button"
2. Take screenshot for record
3. Click next
4. read_page on new step
5. Repeat until review/submit page
Use browser_snapshot to get the full accessibility tree, including iframe content.
Iframe elements appear with refs like "f54eXX" — these refs work with all Playwright tools.
Use browser_fill_form with ref/value pairs from the snapshot:
- fields: [{ name: "First Name", type: "textbox", ref: "ref123", value: "John" }]
For multiple fields at once, pass an array of field objects.
1. browser_click on the dropdown toggle/button to open it
2. browser_snapshot to read the available options
3. browser_click on the correct option
For comboboxes (typeahead): type into the field first, then click the matching suggestion.
Two patterns depending on the form:
Pattern A: Direct setInputFiles (preferred)
Use browser_run_code:
async (page) => {
await page.locator('#resume-input').setInputFiles('/path/to/resume.pdf');
}
Replace '#resume-input' with the actual selector for the file input.
Pattern B: File chooser interception
Use browser_run_code:
async (page) => {
const [fileChooser] = await Promise.all([
page.waitForEvent('filechooser'),
page.locator('button:has-text("Upload")').click()
]);
await fileChooser.setFiles('/path/to/resume.pdf');
}
If a modal appears after upload, use browser_file_upload to complete it.
Playwright handles iframes transparently.
browser_snapshot shows iframe content with refs (e.g., "f54eXX").
These refs work directly with browser_fill_form, browser_click, etc.
No special handling needed — just use the refs from the snapshot.
If browser_click is intercepted by overlays, use browser_evaluate:
element.click();
element.dispatchEvent(new Event('change', {bubbles: true}));
Pass the ref of the radio input element.
User: Help me apply to this job: https://www.linkedin.com/jobs/view/123456789
3plugins reuse this skill
First indexed Jul 14, 2026
npx claudepluginhub hussainronaque/job-apply-plugin --plugin job-applyAutomates filling job applications on Greenhouse, Lever, and Workday via browser automation with resume, cover letter, and personal data. Handles job URLs, recent jobs, or current tab.
Generates personalized answers for job application forms, cover letters, and custom questions using profile.yml, resume.md, evaluations, and company research. Never auto-submits.
Generates ATS-optimized resumes tailored to job postings from master resumes or experience data, producing .docx files via Python rendering.