Skip to content

Commit ef03ca0

Browse files
committed
Three commands, because the heading promised more than one client
Option 1 was headed "Claude Code, Claude Desktop or Cursor" and then gave a single command that only the first of the three can run. A Cursor reader arrived at a `claude mcp add` line with nothing telling them it was not for them. There are three commands now, verified against each vendor's own docs: Claude Code, Codex and Gemini CLI. They do not even agree on syntax - Gemini takes no `--` separator where the other two require it - so each is written out rather than described. The heading no longer lists products, because a list of products goes stale: it asks the question a reader can actually answer, whether they already use an assistant that can run tools. The six clients that take a config file instead are named, with a pointer to the server's README where each one's file is written out - and it is a different file with a different top-level key for several of them, which is why they are not squeezed in here.
1 parent 1fe7f9d commit ef03ca0

6 files changed

Lines changed: 826 additions & 3 deletions

README.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,37 @@
2424

2525
The only question is where the model comes from.
2626

27-
### 1. You already use Claude Code, Claude Desktop or Cursor
27+
### 1. You already use an assistant that can run tools
2828

2929
Your assistant brings the model. You add this browser to it, and nothing changes
3030
about how you work.
3131

32+
**Claude Code:**
33+
3234
```bash
3335
claude mcp add -s user stealth -- uvx invisible-playwright-mcp
3436
```
3537

38+
**Codex:**
39+
40+
```bash
41+
codex mcp add stealth -- uvx invisible-playwright-mcp
42+
```
43+
44+
**Gemini CLI:**
45+
46+
```bash
47+
gemini mcp add -s user stealth uvx invisible-playwright-mcp
48+
```
49+
3650
Then ask your assistant, in the window you already have open:
3751

3852
> Go to news.ycombinator.com and give me the top five titles.
3953
40-
For Claude Desktop, Cursor and the rest, which take a config file instead, the
41-
block to paste is in the [server's README](https://github.com/feder-cr/invisible-playwright-mcp).
54+
Claude Desktop, Cursor, VS Code, Windsurf, Zed and Cline take a config file
55+
instead, and the file is not the same shape for all of them. Each one is
56+
written out in the
57+
[server's README](https://github.com/feder-cr/invisible-playwright-mcp#adding-it-to-your-client).
4258

4359
### 2. You don't, or you want to watch it work
4460

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
---
2+
title: "Automating job applications in Python"
3+
description: "The build-it-yourself route: browser automation plus an LLM API, the four components a real pipeline needs, why naive scripts break on wizards, validation and anti-bot layers, and the honest case for using what already exists."
4+
parent: "Job Application Automation"
5+
nav_order: 3
6+
---
7+
8+
9+
# Automating job applications in Python
10+
11+
Building a job-application bot in Python is a genuinely instructive project: it
12+
touches browser automation, LLM integration, state, and the parts of the web
13+
that push back. This page lays out what the real components are, shows why the
14+
obvious eighty-line script breaks, and closes with the honest note that this
15+
exact project has already been built in the open, because that is literally how
16+
AIHawk started.
17+
18+
One rule this page keeps, like every page under this parent: no job platform is
19+
named, and the examples run against generic forms or pages you serve yourself.
20+
The mechanics are the same everywhere, which is exactly why the generic version
21+
is worth writing down.
22+
23+
## The naive version, to have something to break
24+
25+
Every attempt starts roughly here: Playwright opens the form, an LLM drafts the
26+
answers, the script fills and submits.
27+
28+
```python
29+
from playwright.sync_api import sync_playwright
30+
31+
BACKGROUND = open("background.md").read() # your real history, as text
32+
33+
def draft(question: str) -> str:
34+
# any LLM API; the contract is what matters:
35+
# answer from BACKGROUND only, say "ASK" when the answer is not in it
36+
...
37+
38+
with sync_playwright() as p:
39+
browser = p.firefox.launch()
40+
page = browser.new_page()
41+
page.goto("http://127.0.0.1:8000/application-form") # a page you serve
42+
for field in page.locator("form [data-question]").all():
43+
answer = draft(field.get_attribute("data-question"))
44+
field.fill(answer)
45+
page.click("#submit")
46+
```
47+
48+
On a form you wrote yourself, this works on the first try, which is what makes
49+
the approach look one weekend wide. The distance between this and a pipeline
50+
that survives real pages is the rest of this page.
51+
52+
## The four components a real pipeline needs
53+
54+
**Form detection.** Real forms do not label themselves with `data-question`.
55+
Fields associate with their questions through `<label for>`, `aria-label`,
56+
`aria-labelledby`, placeholder text, or nothing but visual proximity; required
57+
markers and error text sit in separate nodes; some forms arrive inside an
58+
embedded iframe with its own document. Mapping "what is this field asking" to
59+
"which element do I fill" is a real subproblem, and it is where an LLM earns its
60+
place: handed the rendered page, a model can read the form the way a person
61+
does, which is the approach described in
62+
[getting an AI agent to fill out forms](ai-agent-fill-out-forms.md).
63+
64+
**Answer generation.** The model needs your background as grounding and a
65+
contract that keeps it honest: every claim from the provided history, an
66+
explicit escape hatch ("ASK") for questions it cannot answer from it, and no
67+
improvisation. Tailoring - picking which true things to emphasize for this role
68+
- is the value. Fabricating qualifications is a bug, and it is one your own
69+
pipeline will happily ship at scale if the prompt does not forbid it.
70+
71+
**Session persistence.** Applying is logged-in work spread over days. That
72+
means a persistent browser profile rather than a fresh context per run, so
73+
cookies and logins survive; it also means the same browser identity each time,
74+
because a login that hops between fingerprints looks stolen. Plan for a profile
75+
directory and for state you can resume after a crash mid-wizard.
76+
77+
**Pacing.** The component most builds skip and the first one to matter in
78+
production. Per-account and per-IP rate limits are real, and a submission every
79+
few seconds is a signature no fingerprint work can hide. Pacing is jitter
80+
between actions, minutes between applications, a daily cap, and backoff on
81+
anything that looks like throttling. It is also the ethical component: the
82+
volume ceiling is where "my bot" stops being distinguishable from spam, a
83+
lesson this project's own history documents in detail on
84+
[the history page](open-source-job-application-bot.md).
85+
86+
## Why naive scripts break
87+
88+
**Multi-step wizards.** Real applications are three to eight steps with state:
89+
conditional fields that appear based on earlier answers, a review step that
90+
re-renders everything, a back button that resets more than it should. A script
91+
that models "the form" as one page loses to the first wizard it meets. You need
92+
a loop over steps - read, fill, advance, re-read - and idempotent resume,
93+
because step four is where the timeout happens.
94+
95+
**Client-side validation.** Fields validate on blur and on keystroke; masked
96+
inputs reformat as you type; selects are custom widgets that only respond to
97+
real interaction. Setting `element.value` from JavaScript skips the events the
98+
page listens for, so the value silently fails validation, or arrives empty at
99+
submit. Fill through real typing and clicking. It is telling that AIHawk's own
100+
agent, per its README, refuses to set form fields from JavaScript even when it
101+
would be faster, because a page can tell the difference.
102+
103+
**File uploads.** The resume field is a file input, often behind a styled
104+
button, sometimes a drag-and-drop zone. Playwright's `set_input_files` covers
105+
the plain case; the styled cases need the real input located first. And a
106+
tailored resume means generating a file per application, which is its own
107+
pipeline stage.
108+
109+
**The anti-bot layer.** This is the wall that surprises builders most, because
110+
it has nothing to do with your code being correct. Stock automation is
111+
detectable below your script: driver artifacts, headless tells, a fingerprint
112+
inconsistent with the claimed platform, a TLS handshake that does not match the
113+
user agent. No amount of selector work fixes a page that has already decided
114+
what you are. The mechanism layer - fingerprinting surfaces, detection vendors,
115+
network tells - is documented in depth on the
116+
[invisible_playwright wiki](https://github.com/feder-cr/invisible_playwright/wiki),
117+
and the agent-level symptoms in
118+
[why does my AI agent get blocked?](why-does-my-ai-agent-get-blocked.md). If
119+
you stay on the build-it-yourself road, that engine is usable as a library:
120+
[invisible_playwright](https://github.com/feder-cr/invisible_playwright) is a
121+
Firefox patched at the C++ level, driven through the standard Playwright API,
122+
so the code above ports by changing the launch.
123+
124+
## Or use what exists
125+
126+
Here is the honest close. The pipeline this page describes - form reading,
127+
grounded answer generation, persistent sessions, pacing, on a browser built to
128+
look like a real one - is an open-source project you can read instead of
129+
rediscover. [AIHawk](https://github.com/feder-cr/AIHawk) began in 2024 as
130+
exactly this Python bot, reached about 30,000 stars and a wave of press
131+
coverage, and grew into a general web agent on that same hardened engine. It is
132+
MIT-licensed; run it as an interface with `uvx aihawk ui`, script it headless
133+
with `uvx aihawk do "..."`, or take just the browser layer as a library and
134+
keep your own agent logic on top.
135+
136+
Building your own is still a fine choice when the point is learning or when
137+
your flow is unusual. But go in knowing which parts are the actual work: not
138+
the form filling, which a weekend gets you, but the wizard state, the
139+
validation-safe input, the stable identity, and the restraint.
140+
141+
## Conclusion
142+
143+
A real job-application pipeline in Python is four components - form detection,
144+
grounded answer generation, session persistence, pacing - sitting on a browser
145+
that can survive being looked at. The naive script fails on multi-step wizards,
146+
event-driven validation, uploads, and an anti-bot layer that inspects the
147+
browser underneath your code. All four components, on a hardened engine, exist
148+
in the open already; whether you build or adopt, the volume ceiling and the
149+
review-before-submit rule are the parts that keep the pipeline worth running.
150+
151+
## Short answers to the questions that lead here
152+
153+
**Can I automate job applications with Python and Playwright?** Yes, and the
154+
form-filling part is genuinely easy. The real work is wizard state,
155+
validation-safe typing, persistent sessions, pacing, and a browser that does
156+
not advertise itself as automation.
157+
158+
**Which LLM do I need?** Any API model works for answer generation. The prompt
159+
contract matters more than the model: answers only from your provided
160+
background, with an explicit "ask the human" escape for anything else.
161+
162+
**Why does my script's input disappear at submit?** You are probably setting
163+
values from JavaScript, which skips the input events client-side validation
164+
listens for. Type and click like a user; the value then exists the way the page
165+
expects.
166+
167+
**Why does my bot get blocked even though the code works?** Detection operates
168+
below your script: driver artifacts, headless tells, fingerprint and TLS
169+
inconsistencies. See the
170+
[invisible_playwright wiki](https://github.com/feder-cr/invisible_playwright/wiki)
171+
for the mechanism layer; correctness of your Python is not the question being
172+
asked.
173+
174+
**How fast can I safely go?** Slower than the code allows. Jitter inside a
175+
form, minutes between applications, a daily cap you would defend out loud.
176+
Past that ceiling you are building a spam tool with extra steps, and the
177+
account it burns is yours.
178+
179+
**Is there an open-source version already?** Yes -
180+
[AIHawk](https://github.com/feder-cr/AIHawk), which started as exactly this
181+
bot and is now a general web agent on a hardened Firefox. Reading its layout is
182+
a shortcut even if you then build your own.
183+
184+
## Sources
185+
186+
- The [AIHawk README](https://github.com/feder-cr/AIHawk#readme), retrieved
187+
2026-09-03, for the interface and headless commands, the library and MCP
188+
layers, the license, and the agent's refusal to set form fields from
189+
JavaScript.
190+
- [404 Media's October 2024
191+
report](https://www.404media.co/i-applied-to-2-843-roles-the-rise-of-ai-powered-job-application-bots/),
192+
retrieved 2026-09-03, for what the original bot generation actually did in
193+
the field: biographical fill, generated resumes, customized cover letters,
194+
unattended volume.
195+
- This project's own engine documentation, linked throughout, for the
196+
fingerprinting and detection mechanics summarized in the anti-bot section.
197+
198+
**See also:** [getting an AI agent to fill out
199+
forms](ai-agent-fill-out-forms.md) for the form-reading approach in isolation,
200+
[automating job applications with
201+
Claude](automate-job-applications-with-claude.md) for the no-code version of
202+
this pipeline, [the open-source job application bot, and what it
203+
became](open-source-job-application-bot.md) for where the build-it-yourself
204+
road led once, and the
205+
[job application automation hub](guides-job-application-automation.md).
206+
207+
---
208+
209+
*Written by the maintainer of [AIHawk](https://github.com/feder-cr/AIHawk),
210+
which exists because someone built this exact Python project and then spent two
211+
years on the parts this page says are the actual work.*

0 commit comments

Comments
 (0)