---
name: ghost-job-reporter
description: Find, verify, and report ghost jobs, fake job postings, and recruitment-scam lures using a local browser controlled through BrowserOS Neo. Use when the user wants to work through a queue of suspicious job postings, build an evidence packet for each one, and file factual reports with job platforms, impersonated employers, domain abuse desks, and government complaint channels. Reports are filed autonomously: a deterministic policy engine scores each packet and returns SUBMIT, HOLD, or REFUSE before any browser is opened, so the user does not review individual filings. Includes a persistent queue so reporting is systematic across sessions, destination-tiered evidence thresholds, anti-pile-on limits, and a full audit log of everything submitted.
license: MIT
version: 1.1.0
---

# Ghost Job Reporter

A skill for any local AI agent that has browser control. It turns a pile of
suspicious job postings into a queue, verifies each one against primary sources,
builds an evidence packet, and drives a real browser to file the report.

## What this skill is for

Job seekers get buried by ghost jobs, evergreen requisitions, and outright
recruitment scams. This skill makes it possible to work through them
systematically instead of one at a time, and to file reports that are actually
worth filing.

## What this skill is NOT for

Read this before you start, because getting it wrong gets the user's accounts
banned and creates legal exposure for them.

- **Not a mass-reporting bot.** One report per posting, per destination. No
  repeat filings. No filing on a posting the user never engaged with.
- **Not approval-gated, but not unaccountable either.** You file autonomously, but
  only for packets that `scripts/autonomy_guard.py` clears. The guard is the
  control. You are not. See "The autonomy gate" below.
- **Not an accusation engine.** Nothing here concludes that a company is
  committing fraud. It files observed facts and lets the recipient decide.

## The three rules that override everything else

1. **Never file anything the guard has not returned SUBMIT for.** Run
   `scripts/autonomy_guard.py` against the packet and obey the exit code: 0 files,
   1 waits, 2 never files. The same packet plus the same policy always produces
   the same answer, so there is no way for you to reason your way past a REFUSE.
   If the guard says HOLD or REFUSE, record the item with that status and move on.
2. **Never state or imply that a company is fraudulent, criminal, or a scam.**
   Report only observed facts. "This requisition has been listed since
   6 March 2026 and does not appear on the employer's own careers page" is a
   fact. "This company is running a ghost-job scam" is an accusation, and it is
   defamation if it is wrong.
3. **Never treat an unreachable page as absence.** If the employer's careers page
   timed out, 403'd, or errored, record that check as failed and discard it. Only
   a page you successfully fetched and which did not list the role counts as
   absence. The guard enforces this, and you must too, because a wrong entry in
   the packet is something the guard cannot detect.

## Bootstrap the browser automatically

Do not ask the user to install BrowserOS neo before trying to work. First run:

```bash
python3 scripts/bootstrap_browseros.py
```

The bootstrapper checks for an existing BrowserOS neo process or local install.
If it is missing, it downloads the matching official release asset from the
`browseros-ai/BrowserOS` GitHub releases, installs or launches it for the host
OS, and waits for the local CDP endpoint. It supports macOS and Windows, with a
best-effort Linux AppImage path. It never downloads a random browser, follows
third-party mirrors, or installs a browser extension as a substitute for Neo.

After it succeeds, use the bundled `browseros_driver.mjs` or the available
BrowserOS MCP tools. Read the live page state after every navigation and
interaction; locate controls from the current page rather than stale selectors.
If the browser is installed but not ready, launch it and retry once.

Continue without asking the user for routine setup decisions. Pause only when a
human action is genuinely required: first-run browser permission, account login,
MFA, CAPTCHA or unusual-activity challenge, an unknown report destination,
missing facts needed to complete a form, or a policy decision the guard cannot
make from evidence. Never request or handle passwords, MFA codes, recovery codes,
or session tokens.

## Workflow

### 1. Build the queue

Inputs can be anything the user has: a list of job URLs, a spreadsheet, the
output of a scanner, or postings they paste one at a time.

```bash
python3 scripts/report_queue.py add --url "https://example.com/jobs/123" \
    --company "Example Corp" --title "Senior Sysadmin" --source "indeed"
python3 scripts/report_queue.py import --file postings.csv
python3 scripts/report_queue.py next          # show the next item to work
python3 scripts/report_queue.py list --status pending
python3 scripts/report_queue.py stats
```

The queue lives in SQLite next to the script. It survives restarts and sessions,
which is the whole point: reporting is a multi-session chore.

### 2. Verify before you report

For each queue item, gather what you can from primary sources. Do not skip this
and go straight to filing.

Minimum checks:

- **Is the requisition on the employer's own careers page?** Search
  `site:employer-domain` for distinctive words from the title, and check the
  employer's own job board. Record whether it is present, absent, or unknown.
- **Where does the Apply control actually go?** Resolve the real destination.
  The employer's own domain or a recognised applicant tracking system
  (Greenhouse, Lever, Workday, iCIMS, Ashby, SmartRecruiters, Workable,
  BambooHR, Jobvite) is normal. Anything else gets recorded and investigated.
- **When was it published, per the employer's own system?** Where a board
  exposes a published date or a requisition ID, use it. That beats any estimate.
- **Does the recruiter or sender verify against the employer?** Find the person
  on the employer's own site or a corporate directory. A social profile alone is
  not verification.
- **Domain facts, if the apply path is unfamiliar:** registration date,
  registrar, hosting, and mail authentication posture.

If the user has the `ghost-job-investigator` skill available, its
`scripts/osint_domain.py` does the domain half of this in one command and its
`references/evidence-rubric.md` defines the scoring language. Use it rather than
reimplementing it.

### 3. Apply the evidence bar

**Do not create a report unless at least one of these is true:**

| Signal | Why it is reportable |
|---|---|
| Requisition absent from the employer's own careers page while actively advertised elsewhere | A concrete, checkable discrepancy the employer can answer |
| Apply destination is not the employer and not a recognised ATS | Concrete misrepresentation of who is hiring |
| Domain registered recently while impersonating a long-established company | Concrete impersonation |
| The posting demands money, gift cards, crypto, a cheque deposit, SSN, bank details, ID photos, tax forms, passwords, or MFA codes | Concrete fraud indicators, matching published government advisories |
| The "interview" requires running an unvetted binary, repo, or package | Concrete malware-delivery pattern |
| The same requisition appears under a rolling sequence of requisition IDs | Pattern evidence the employer can confirm or deny |

**Not sufficient on its own:** how old the posting is, how many times it was
reposted, how many applicants it has, how the salary looks, or how the job
description is written. These are reasons to investigate. They are not
reportable findings.

### 4. Assemble the evidence packet

For each item, capture and store:

- Exact URL, and the platform it was found on
- Company name as listed, job title as listed, location as listed
- Requisition ID if published, and the employer's own published date if exposed
- Resolved Apply destination
- Screenshot of the posting as displayed, saved to disk
- The date and time of capture
- Every check you ran and its result, including the ones that came back clean
- What the user personally experienced, if anything (applied, interviewed,
  contacted, paid, sent documents)

Write it to `packets/<queue-id>.json` and keep the screenshots beside it. A
report without a screenshot and a timestamp is a report the recipient can
discard.

### 5. Choose ONE destination per posting

Do not file the same complaint everywhere. Pick the one with the best chance of
removing the trap, in this order:

1. **The platform hosting the posting.** Fastest real effect, because it removes
   the listing for the next person.
2. **The employer being impersonated**, via a contact published on their own
   website, when the posting misrepresents them.
3. **The abuse desk of the domain** hosting the apply path, when it is not the
   employer and not a real ATS.
4. **Government channels** — IC3 for cybercrime and financial loss, FTC for
   deceptive practices, the relevant state attorney general for consumer
   matters. Appropriate when money, credentials, or malware were involved.
5. **The Better Business Bureau** — only if the user had a genuine marketplace
   relationship with the business. The BBB is not a fraud hotline, and filing
   about a listing the user never engaged with wastes everyone's time.

See `references/report-wording.md` for fact-only wording per destination.

### 6. Drive the browser

See `references/setup-browseros-neo.md` to get BrowserOS Neo running and
connected, and `references/platform-report-paths.md` for how to locate the
report control on each platform.

The pattern is always the same:

1. Run `scripts/bootstrap_browseros.py` and confirm the CDP/MCP connection.
2. Open the posting or the report form in the user's own logged-in browser.
3. Locate the report control by **searching the live page for it**, never by
   hardcoding a selector. Platforms change their UI constantly and a stale
   selector silently clicks the wrong thing.
4. Fill every field with the facts from the packet.
5. Screenshot the completed form **before** submitting, and save it beside the packet.
6. Run the guard on the packet. Only if it returns SUBMIT, click the submit
   control. Then screenshot the confirmation or reference number.

### 7. The autonomy gate

This is the control that replaces human review. Do not skip it and do not
second-guess it.

```bash
python3 scripts/autonomy_guard.py --packet packets/12.json --queue-db report_queue.db
# exit 0 = SUBMIT   1 = HOLD   2 = REFUSE
```

Always pass `--queue-db`, never an exported history file. The guard reads prior
submissions straight from the queue database, which is what makes the pacing and
anti-pile-on rules impossible to bypass by forgetting to re-export.

What the guard enforces, in short:

- At least one **hard signal** (an observed act) plus at least one independent
  corroborating signal. Circumstantial signals such as age, reposting, or
  requisition churn never justify a report alone, no matter how many stack up.
- **Unreachable is never absent.** A claimed absence requires
  `page_reachable: true` on that check.
- The packet must state what it does **not** claim, and must record that the user
  actually observed the posting.
- **Destination tiers.** Platform and employer reports need 50. Registrar, host,
  and mail-provider reports need 60. IC3, FTC, and state attorney general reports
  need 70 **and an observed conduct signal** — money, ID documents, credentials,
  or code execution. A posting being old is reportable to the platform, never to
  a regulator.
- Company denylist, exact-URL deduplication, daily caps, per-platform caps,
  minimum spacing, and one report per company per 30 days.

Read `references/autonomy-policy.md` for the full rationale. If the guard returns
HOLD, mark the item `insufficient-evidence` or leave it pending and move on.
If it returns REFUSE, mark the item and never file it.

After a cleared submission, record it:

```bash
python3 scripts/report_queue.py done --id 12 --destination "indeed" \
    --reference "REF-12345" --outcome "acknowledged" --autonomous
```

### 8. Pace it

The guard enforces these, so you do not have to remember them. Do not attempt to
work around them.

- Maximum **10 submissions per day**
- Maximum **4 per platform per day**
- Minimum **5 minutes between submissions to the same platform**
- One report per company per **30 days**
- Stop immediately on any CAPTCHA, MFA prompt, unusual-activity warning, or
  rate-limit page. Do not attempt to work around it. Record it in the queue, tell
  the user, and end the run.
- Never use multiple accounts, VPN rotation, or any technique to evade
  detection. If a platform is blocking the user, that is the platform's answer.

The queue stores the counts, so a later session does not restart them.

## Reporting outcomes honestly

Most reports get no reply. Some get an automated acknowledgement. A few result
in removal. That is the normal distribution and the user should hear it from you
before they start, not after twenty silent submissions.

Never tell the user a report "worked" unless you have a confirmation, a
reference number, or observed removal. "Filed, no response yet" is the honest
answer and it is fine.

## What to do when the evidence is thin

Say so. Mark the item `insufficient-evidence` with a note explaining what was
missing, and move on. A queue that produces honest "cannot verify" results is
working correctly. A queue that produces a report for every item is broken.

## Files in this skill

- `references/setup-browseros-neo.md` — install and connect BrowserOS Neo
- `references/platform-report-paths.md` — finding the report control per platform
- `references/report-wording.md` — fact-only wording per destination
- `references/autonomy-policy.md` — how the guard decides, and why
- `references/guardrails.md` — the full rule set, including the legal reasoning
- `scripts/report_queue.py` — the persistent queue and the audit log
- `scripts/autonomy_guard.py` — the SUBMIT / HOLD / REFUSE decision engine
- `scripts/autonomy_policy.json` — the policy the guard applies. Read it before the first run.
- `scripts/browseros_driver.mjs` — zero-dependency Node CDP driver for BrowserOS Neo
- `scripts/bootstrap_browseros.py` — detects, downloads, installs, launches, and validates BrowserOS neo
- `templates/packet.example.json` — evidence packet shape


---

# Appendix: inlined references
The following reference documents ship alongside this file in the full package. Everything needed to operate the skill is included below.


---

# guardrails.md

# Ghost Job Reporter — guardrails

These rules exist because the failure modes are real, not theoretical. Read them
before you run this skill on anything.

## 1. One report per posting, per destination

Mass-filing the same complaint to many channels does not increase the chance of
action. It marks the sender as a bulk reporter, and abuse desks route bulk
reports to the bin. Worse, coordinated reporting campaigns against a named
company are the definition of a pile-on, and they can cross into harassment.

File once, to the best destination, with the best evidence. Then stop.

## 2. The autonomy gate decides, not you

Reports are filed autonomously. There is no per-filing human review. That is the
point of the tool, and it puts the entire quality burden on one component:

```
scripts/autonomy_guard.py  --  exit 0 SUBMIT, 1 HOLD, 2 REFUSE
```

The agent does not decide whether to file. The guard decides, from the evidence
packet, against `scripts/autonomy_policy.json`. The same packet plus the same
policy always produces the same answer, so there is no reasoning path around a
REFUSE.

**Rules for the agent:**

- Never click submit unless the guard returned SUBMIT for that exact packet.
- Never edit the policy mid-run to make a REFUSE pass. If a packet is refused,
  the correct response is to record the status and move on.
- Never inflate a packet to clear the bar. Recording `page_reachable: true` for a
  page you did not successfully fetch is the single worst thing you can do here,
  because the guard cannot detect it and the resulting report goes out under the
  user's name.
- Always pass `--queue-db` so the pacing and anti-pile-on rules are read from the
  live database rather than a stale export.

**Why this substitutes for human review:** reviews only work if they happen. A
queue of fifty postings with a human gate becomes a queue of four filings and
forty-six abandoned items. A deterministic gate means every item gets the same
standard applied, the caps and anti-pile-on rules cannot be forgotten, and the
one irreversible action is governed by written rules rather than by how carefully
someone was paying attention at the time.

**What the user still owns:** reading `autonomy_policy.json` once before the
first run, and the audit log afterward. Every filing is recorded with its
destination, reference number, and the text that went out, so a wrong filing can
be found and corrected.

## 3. Facts, never conclusions

Write only what you observed, with the source.

| Do not write | Write |
|---|---|
| "This company is running a ghost-job scam." | "The requisition does not appear on the employer's careers page, and has been advertised on [platform] since [date]." |
| "This is a North Korean malware operation." | "The apply link resolves to [domain], registered [date], which is not affiliated with [employer]. The posting asks candidates to run an attached archive." |
| "They stole my data." | "On [date] I submitted [document type] through [URL]. I have since received [specific observed consequence]." |

Never name a person as a criminal. Never speculate about who is behind a domain.
Never assert an employer's intent. Intent is the one thing a report cannot
evidence, and it is the thing that makes an accusation defamatory.

## 4. First-hand only

Report on postings the user actually saw, and only on what they actually did.
Do not file on behalf of third parties, do not report a posting the user only
heard about, and do not aggregate other people's complaints into one filing.

The BBB and most consumer channels specifically require a genuine marketplace
relationship. A listing someone looked at and decided not to apply to is not one.

## 5. Do not report these

- A posting that is merely old. Age is not a violation.
- A posting that is merely reposted. Reposting is not a violation.
- A company with bad reviews.
- A company whose careers page was unreachable during your check. That is your
  network, not their conduct. Retry before concluding anything.
- A recruiter whose only sin is a free-mail address, with no other signal.
- A legitimate employer whose posting is genuinely hard to fill.

Each of these generates a false report, and enough false reports make the real
ones worthless.

## 6. Platform terms

The browser used here is the user's own, logged in, under their account.
Automating it is still automation, and LinkedIn's User Agreement prohibits it.

Practical consequences the user should know before starting:

- LinkedIn is the most aggressive about detecting automation, and the most likely
  to restrict an account. Pace it hardest there, or use the platform's own
  in-page report control manually and let the agent do only the research.
- Indeed and Dice are lighter-touch, but both have bot detection.
- Rate limits in this skill are deliberately conservative. Do not raise them.

If a platform challenges, blocks, or warns: stop. Do not retry through a
different route. An account restriction costs the user far more than the report
was worth.

## 7. No evasion

No account rotation, no proxy or VPN rotation to dodge detection, no CAPTCHA
solving services, no spoofed fingerprints. If a platform does not want automated
submissions, the correct response is to stop automating on that platform.

## 8. Privacy

Evidence packets contain the user's own data, and sometimes a real recruiter's
name and contact details. Keep packets local. Do not upload them, do not commit
them to a public repository, do not post them publicly. Do not publish a list of
"bad companies" anywhere — that is the single fastest way to convert a
consumer-protection tool into a defamation claim.

## 9. Reporting honestly

Most reports produce nothing visible. Tell the user that up front.

Never claim a report succeeded without a reference number, a confirmation, or
observed removal. "Submitted, no response" is a complete and honest status.

## 10. Stop conditions

End the session immediately if any of these occur:

- CAPTCHA, MFA prompt, or an "unusual activity" warning
- The user asks you to file something you cannot evidence
- You cannot determine the report destination with confidence
- You have hit the daily cap, or the guard has returned HOLD for the item
- The evidence turns out to describe a legitimate employer

Stopping is a correct outcome. Report why and stop.


---

# autonomy-policy.md

# The autonomy policy

`scripts/autonomy_guard.py` decides whether a report gets filed. The agent does
not. This document explains what the guard enforces and why each rule exists.

## The shape of the decision

```bash
python3 scripts/autonomy_guard.py --packet packets/12.json --queue-db report_queue.db
```

Exit codes:

| Code | Decision | Meaning |
|---|---|---|
| 0 | SUBMIT | Cleared. The agent files it and records it with `done --autonomous`. |
| 1 | HOLD | Missing evidence, below the score bar, or pacing. Gather more, retry later. |
| 2 | REFUSE | Must not be filed. Do not retry unless the underlying facts change. |

Add `--json` for machine-readable output. The agent parses `decision` and stops
on anything other than `SUBMIT`.

**Pass `--queue-db`, not a history file.** The guard reads prior submissions
straight from the queue database so the pacing and anti-pile-on rules cannot go
stale. An exported history file will silently stop protecting you the moment
someone forgets to re-export it.

## Hard signals versus soft signals

Hard signals describe something that was observed to happen. Soft signals
describe circumstances.

```
HARD: absent-from-employer-board   apply-destination-mismatch   brand-impersonation
      money-request   check-deposit   pii-request   credential-request
      code-execution-lure

SOFT: stale-age   reposted   req-id-churn   title-churn
      salary-oddity   recruiter-unverified
```

A posting can be two years old, reposted nine times, and carry three requisition
IDs, and that is still not enough to file a report on its own. Soft signals
justify investigation. They never justify a filed report.

The reverse also holds: one hard signal with nothing corroborating it produces
HOLD, not SUBMIT. The policy requires at least two independent signals.

## Unreachable is not absent

The most important rule in the file.

```json
"require_reachable_page_for_absence": true
```

If a packet claims a requisition is missing from the employer's careers page, the
guard requires `page_reachable: true` on that check. A timeout, a 403, a 404 on
the page itself, or a network blip is **not** absence, and the guard refuses the
packet rather than letting it through.

Without this rule, an autonomous pipeline will eventually file a fraud report
against a legitimate employer because their careers page was down for ten
minutes. Every reported absence must be a page that was fetched successfully and
did not list the role.

The same applies to any check whose result is `timeout`, `error`, `blocked`,
`unreachable`, `403`, or `404`: the check is discarded and cannot support a
finding.

## Destination tiers

Not all reports carry the same stakes, so they do not carry the same bar.

| Tier | Destinations | Min score | Notes |
|---|---|---|---|
| `platform` | indeed, dice, ziprecruiter, glassdoor, monster, careerbuilder, flexjobs, wellfound | 50 | Reversible. The platform re-checks and delists or does not. |
| `employer` | the employer directly | 50 | They can correct their own listing. |
| `consumer` | bbb | 50 | Also requires a genuine marketplace relationship. |
| `infrastructure` | registrar, host, mailprovider | 60 | A takedown request against a domain or mailbox. |
| `authority` | ic3, ftc, attorneygeneral | 70 | **Requires an observed conduct signal.** |

### Why authority reports need conduct

```json
"authority_requires_conduct_signal": true,
"conduct_signals": ["money-request", "check-deposit", "pii-request",
                    "credential-request", "code-execution-lure"]
```

Regulators and law enforcement act on conduct. A posting being old, stale, or
absent from a careers page is a listing-quality problem, and the platform is the
right recipient. Sending it to the FBI wastes an investigator's time, dilutes the
signal for real cases, and makes the tool's reports worth less to everyone using
it.

So: a ghost job with strong circumstantial evidence can be filed with the
platform. It can never be filed with a regulator. A job that asked for money or
ID can be filed with either.

## Rate limits and anti-pile-on

```json
"max_per_day": 10,
"max_per_platform_per_day": 4,
"min_minutes_between_same_platform": 5,
"max_per_company_per_days": 30
```

`max_per_company_per_days` is the anti-pile-on rule, and it is the one value you
should not lower. One report per company per 30 days. Filing repeatedly against
the same company is not consumer advocacy, it is harassment, and it is the
fastest way to have your reports ignored and your account actioned.

An exact URL can never be filed twice, regardless of any other setting.

## Packet requirements

```json
"require_not_claimed": true,
"require_user_observed": true,
"bbb_requires_relationship": true
```

Every autonomous filing must state what it does **not** claim. A packet without
that section is refused. This is not decoration: it is what keeps the wording
factual and prevents the report from asserting intent on your behalf.

There must also be a record that you actually observed the posting, and BBB
complaints require a genuine marketplace relationship because that is BBB's own
rule.

## Tuning

Everything above lives in `scripts/autonomy_policy.json`. Edit it once, before
your first run, and understand that every loosened value increases the chance of
filing a false report against a legitimate employer.

To put yourself back in the loop without losing any of the machinery:

```json
"mode": "approve-before-submit"
```

The guard still runs, still scores, still refuses the bad ones. It just waits for
you before the final click.

## What the guard cannot do

Be honest about the boundary. The guard reasons over what the packet says. If the
agent recorded a check that never happened, or recorded `page_reachable: true`
without fetching the page, the guard cannot tell. That is why
`packet.example.json` records the fetch time, the status code, and the source URL
for every check: so a wrong entry is visible in the audit log rather than
invisible.


---

# setup-browseros-neo.md

# Setting up BrowserOS Neo

BrowserOS Neo is a standalone local GUI browser that your agent controls. Your
agent supplies the reasoning; Neo supplies the visible browser, your logged-in
sessions, tabs, navigation, and actions.

Download: https://www.browseros.com/neo/

## Why a real local browser instead of a headless one

Job platforms and complaint portals sit behind Cloudflare, Akamai, and DataDome.
Headless browsers get blocked, and the block looks like "the site is down" when
it is not. A real browser with real sessions gets through, and — more importantly
for this skill — it can hold the user's own logins, which is what makes filing a
report from their account possible at all.

## Install and connect

1. **Install the app.** Get it from the official page above. Do not substitute an
   npm package or a different browser with a similar name.
2. **Launch it and sign in** to the sites you want the agent to use. The agent
   never handles your passwords or MFA codes — you do that yourself, in the
   browser window.
3. **Confirm the agent interface is enabled.** Neo exposes:
   - an MCP server, default `http://127.0.0.1:9010/mcp`
   - a CDP endpoint for direct control, default port 9110, sometimes 9111
   Use whatever port the app actually displays. Do not assume.
4. **Register it with your agent.** Use your agent's supported MCP configuration.
   Do not hand-write connectors or invent config files.
5. **Verify for real.** Have the agent open `https://example.com`, read the title
   back, and take a screenshot. A reachable port is not a working connection.

## Security, and this matters

- **Anyone who can reach the MCP or CDP endpoint controls your signed-in
  browser.** Keep it on localhost. Never expose it publicly, never tunnel it to
  a shared address.
- Treat the browser as holding every credential you are logged into. That is
  exactly why the submission gate exists: the agent is acting with your full
  authority.
- **Never paste passwords or MFA codes into the agent.** Log in yourself. If a
  login prompt appears mid-task, the human handles it.

## Verifying the connection with the bundled driver

`scripts/browseros_driver.mjs` is a zero-dependency Node 22+ driver. It reads the
live CDP port from Neo's config rather than assuming one.

```bash
node scripts/browseros_driver.mjs list
node scripts/browseros_driver.mjs open "https://example.com"
node scripts/browseros_driver.mjs eval "document.title"
node scripts/browseros_driver.mjs screenshot /tmp/shot.png
```

Neo's config file location varies by platform. Common locations:

- macOS: `~/Library/Application Support/BrowserOS Neo/`
- Windows: `%APPDATA%\BrowserOS Neo\`
- Linux: `~/.config/BrowserOS Neo/`

The driver searches these and falls back to probing 9110 and 9111. If it cannot
find the port, check the port in the app's own settings rather than guessing.

## When a page resists

Real-world notes from driving these kinds of portals:

- **Locate elements at the moment you click them.** React and Angular re-render
  and shift layout. Coordinates measured a few seconds ago land on the wrong
  element.
- **A plain JS `.click()` works on React.** It often fails on Angular, which
  intercepts synthetic events. For Angular, use real CDP mouse events at the
  element's centre, after scrolling it into view.
- **Read input values with `.value`, never `innerText`.** `innerText` does not
  show what is in a form field.
- **For React and MUI fields**, set the value through the native setter and
  dispatch `input` and `change` events. Assigning `.value` directly is silently
  discarded.
- **After any interaction, re-read the page state.** Do not assume a click
  worked. Confirm it.
- **Back off on challenges.** Five or more rapid navigations to the same
  Cloudflare-protected domain will trigger a challenge even on a real browser.
  If you get a "Just a moment" page, stop and hand it to the human.

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| Connection refused on the CDP port | App not running | Launch BrowserOS Neo |
| Port reachable but no tools | MCP server disabled in the app | Enable it in the app settings |
| Agent drives a tab that gets hijacked or closed | Another automation is sharing the instance | Do create, navigate, and read inside one process |
| Page loads but reads as empty | Content is inside an iframe | Attach to the frame's own context |
| Repeated blocks | Too fast | Wait 30 to 60 seconds, or move to a different task |


---

# platform-report-paths.md

# Finding the report control, per platform

## The rule that matters more than any selector

**Never hardcode a CSS selector or a button label.** These interfaces are
rebuilt constantly. A stale selector does not fail loudly — it clicks whatever
now occupies that position, which is how an agent ends up doing something
unintended on a page it is logged into.

Every time, discover the control from the live page:

1. Read the accessibility tree or the visible text of the page.
2. Search for report affordances: "Report", "Flag", "Report this job",
   "Report a problem", "Report abuse", "Give feedback", "More options", the
   overflow menu (`...`), a flag icon, or a shield icon.
3. Confirm the candidate is the right control by reading its accessible name and
   the text of its container. Then click it.
4. Re-read the page. Confirm the expected dialog appeared. If it did not, stop
   and re-inspect rather than clicking again blindly.

A short pause to inspect costs seconds. A wrong click on a logged-in job
platform can cost the user their account.

## Where these controls usually live

Treat this as a starting hint, then verify on the live page.

### Job platforms

- **LinkedIn** — on the job page, an overflow (`...`) menu near the top of the
  posting, or a "Report this job" option within it. Recruiter profiles have a
  separate `...` menu with report and block. Expect a reason picker and a text
  field. **LinkedIn is the most aggressive platform about automated access.
  Strongly consider doing the actual filing by hand here and using the agent only
  for research and wording.**
- **Indeed** — a "Report this job" or flag affordance near the bottom of the job
  page, and often in the job card's overflow menu. Reason picker plus optional
  details.
- **Dice** — a report or flag control on the posting, often near the apply
  button or in the posting header.
- **ZipRecruiter, Glassdoor, Monster, CareerBuilder, FlexJobs** — the same
  pattern: an overflow menu or a flag control on the posting.
- **Wellfound** — report control on the role page, and a separate one for
  recruiter accounts.
- **Google Jobs** — listings surface a feedback or report option on the listing
  itself. Use the source platform's own control when you can reach it, since that
  removes the listing at the origin.

### Impersonated employers

Do not use a generic contact form. Find, on the employer's own website:

- a `security@`, `abuse@`, or `trust@` address
- a responsible-disclosure or security page
- a brand-protection or legal contact

Search the site for "security", "report", "disclosure", and "brand". A message to
a company's security team saying "your brand is being used in a job posting at
[URL]" gets action. A message to a sales form does not.

### Domain abuse desks

If the apply destination is not the employer and not a recognised applicant
tracking system, the domain's own abuse channels are the target:

- **Registrar abuse contact** — from the domain's RDAP record. This is the
  `abuse` email in the registration data, and it is the highest-leverage contact
  for impersonation domains.
- **Hosting provider abuse desk** — from the resolved IP's ASN and organisation.
  Most have an `abuse@` address or a web form.
- **Mail provider abuse** — if the lure came by email, the MX provider's abuse
  address. Report the full raw headers, not a forwarded copy.
- **CDN or proxy** — if the domain sits behind a proxy, the proxy's abuse form
  handles impersonation and phishing.

### Government and consumer channels

- **IC3** — `ic3.gov`. Web form. Use when there is cybercrime, malware, or
  financial loss.
- **FTC** — `reportfraud.ftc.gov`. Deceptive practices, including job scams.
- **State attorney general** — consumer protection complaint form for the
  user's state.
- **BBB** — `bbb.org`. Only with a genuine marketplace relationship.
- **Identity theft** — `identitytheft.gov` if documents were handed over.

### Malware and blocklists

- **Google Safe Browsing** — report a phishing or malware URL.
- **Microsoft** — report an unsafe site to SmartScreen.
- **URLhaus / Abuse.ch** — submit a malicious URL or host for distribution to
  blocklist consumers. Requires a free account.

## If the control cannot be found

Say so and stop. Do not guess at a URL for a report form, and do not fabricate a
destination. Record the item as `destination-unclear` with a note about what the
page looked like, and let the human handle it manually. A report filed to the
wrong place is noise, and noise is what makes real reports get ignored.


---

# report-wording.md

# Fact-only wording per destination

Every template below follows the same structure: **what I observed, where, when,
and what I did.** No conclusions about intent. No adjectives. Short.

Length discipline: most abuse desks read the first three lines. Put the URL and
the specific discrepancy there.

Fill the bracketed fields from the evidence packet. Delete any line you cannot
support with a capture.

---

## Job platform (LinkedIn, Indeed, Dice, ZipRecruiter, Glassdoor)

> **Subject:** Job posting report — [job title] at [company as listed]
>
> Posting URL: [URL]
> Company as listed: [company]
> Job title as listed: [title]
> Location as listed: [location]
> Requisition ID, if shown: [id]
>
> I am reporting this posting because of a specific discrepancy, not because it
> is old.
>
> [Pick the one that applies:]
>
> - This role does not appear on the employer's own careers page. I checked
>   [URL] on [date]. The employer's board shows [observed state].
> - The Apply control does not go to the employer or to a recognised applicant
>   tracking system. It resolves to [domain], which is not affiliated with
>   [employer].
> - The posting requires [payment / a cheque deposit / gift cards /
>   cryptocurrency] before employment.
> - The posting asks applicants to submit [SSN / bank details / ID photographs /
>   tax forms] before any interview.
> - The hiring process requires downloading and running [file or package] outside
>   any normal interview process.
>
> I captured a screenshot on [date] at [time], saved at [path].
>
> I am not making a claim about the employer's intent. I am reporting what the
> posting and its apply path actually do.

---

## Impersonated employer (their security or brand team)

> **Subject:** Your brand is being used in a job posting at [URL]
>
> Hello,
>
> I found a job posting advertising a role at [company] at [URL], first observed
> [date].
>
> Two things suggest it is not yours:
>
> 1. The role does not appear on [company]'s careers page at [URL], which I
>    checked on [date].
> 2. The Apply control resolves to [domain], registered [date], hosted at
>    [hosting provider]. That is not [company]'s domain and not an applicant
>    tracking system you appear to use.
>
> The posting also asks candidates to [specific ask]. I have not interacted with
> the poster beyond viewing the listing.
>
> Screenshot and captured page details: [path]
>
> I am not claiming this is your posting or that you are involved. I am flagging
> it in case your brand is being used without your knowledge.
>
> [name]

---

## Domain registrar abuse desk

Registrar abuse addresses are in the domain's RDAP record. Find the `abuse`
contact there; do not guess at a generic address.

> **Subject:** Abuse report — impersonation domain [domain]
>
> Registrar: [registrar from RDAP]
> Domain: [domain]
> Registered: [date from RDAP]
> Hosting: [ASN and organisation]
>
> This domain is being used to impersonate [company] in a job posting.
>
> The posting at [platform URL] advertises a role at [company] and directs
> applicants to apply at [URL on this domain]. The domain is not affiliated with
> [company], which operates at [company domain].
>
> The registration date of [date] is well after [company]'s establishment, and
> the domain does not resolve to [company]'s infrastructure.
>
> Requested action: review the domain's registration and the content being served
> from it for impersonation and possible fraud.
>
> Captures: [path]
>
> [name]

---

## Hosting provider abuse desk

> **Subject:** Abuse report — impersonation and suspected fraud on [IP or host]
>
> IP: [ip]
> ASN / organisation: [asn]
> Domain: [domain]
> First observed: [date]
>
> Content on this host is being used to impersonate [company] in a recruitment
> fraud attempt.
>
> [Same two or three factual bullets as the registrar report.]
>
> Requested action: review the hosted content for impersonation and fraud.
>
> Captures: [path]

---

## Mail provider abuse desk

Only when the lure arrived by email. Include the **full raw headers** — not a
forwarded copy, which destroys them.

> **Subject:** Abuse report — recruitment fraud sent from [sending domain]
>
> Sending address: [address]
> Return-Path / envelope sender: [from headers]
> Sending IP and authentication results: [from headers]
> Date and time: [from headers]
> Subject line: [as received]
>
> This message impersonates a recruitment process for [company]. The domain
> [sending domain] is not affiliated with [company], which operates at
> [company domain].
>
> The message asks the recipient to [specific ask].
>
> Full headers attached. I have not replied to the message or clicked any link
> beyond capturing the URL text.

---

## IC3

Use when there was cybercrime, malware, or financial loss. Be specific about
losses — amounts, dates, method.

> I am reporting a fraudulent recruitment contact.
>
> On [date] I was contacted about a role at [company]. Contact came through
> [platform / email / messaging app].
>
> The process required me to [download and run X / provide Y / send Z].
>
> [If applicable:] I incurred a loss of [$ amount] on [date] via [method].
>
> Evidence retained: [screenshots, file hashes, headers, URLs, transaction
> records].
>
> I am not certain who is responsible. I am reporting the observable facts.

---

## FTC

> I am reporting a deceptive job posting / recruitment practice.
>
> Posting URL: [URL], first observed [date].
>
> [One or two factual bullets on the specific deception.]
>
> I [did not apply / applied and was asked for X / paid Y].
>
> Evidence: [path]

---

## What not to include anywhere

- Accusations of criminal intent, or naming an individual as a criminal
- Your theory of who is behind it
- Links to other people's complaints as support
- Anything you did not personally observe
- Emotive language, exclamation marks, or ALL CAPS
- Speculation about the number of other victims

Stick to the four W's: what, where, when, and what you did.


---

# Appendix: scripts
This single-file version omits the scripts because they are not markdown. The full package includes:
- `scripts/report_queue.py`
- `scripts/autonomy_guard.py`
- `scripts/autonomy_policy.json`
- `scripts/browseros_driver.mjs`
- `scripts/bootstrap_browseros.py`

Without `autonomy_guard.py` you cannot apply the submission policy as written. Either download the full package, or reimplement the policy checks described in `autonomy-policy.md` and refuse to file unless every one of them passes.
