| name | ghostframe-diagnose-block |
| description | Investigate why a specific site blocks automation when the four public detectors pass. Use when a target site (Cloudflare/DataDome/Akamai/PerimeterX/Imperva/Kasada) returns 403 / interstitial / CAPTCHA on first navigation but `bot.sannysoft.com`, `arh.antoinevastel.com`, `creepjs`, and `pixelscan` all pass. Walks the six detection layers from the cheapest probe to the most expensive. |
Diagnose a bot block
When the public detectors pass but a target still blocks, the target is checking something the detectors don't. Walk the six layers in docs/detection-signals.md in order of cost.
How to run these
Use the mcp__ghostframe__* tools, one call per step. The snippets below are shown as
CLI commands for readability; the tool takes the same arguments. Only shell out to
ghostframe <tool> if you are working outside an MCP session — it costs a process
spawn per call and shell-quoted JavaScript breaks easily.
navigator.userAgentData is gated to secure contexts. Read it on an HTTPS page, never
on about:blank, or it comes back empty and looks like a failure it is not.
Default scope
Operates on the currently selected page after a failed navigation to the target. Capture the failure response first; the body and headers are evidence.
Workflow
1. Capture the block
Reproduce on a fresh page so you have a clean response:
ghostframe new_page "<target URL>"
ghostframe list_network_requests --resourceTypes Document
ghostframe get_network_request --reqid <id of the document request> --requestFilePath block-req.md --responseFilePath block-res.md
ghostframe take_screenshot --fullPage true --filePath block.png
Read block-res.md for:
- HTTP status (403, 429, 503, 200 with challenge JS).
Server, cf-mitigated, x-datadome, set-cookie (challenge cookie), x-akamai headers — they tell you which vendor flagged you.
- Body content — JS challenge code, an interstitial title.
Read block-req.md for:
- The headers we sent. Compare against a real browser hitting the same URL.
2. Six danger signs (cheapest probe, do these first)
() => ({
webdriver: navigator.webdriver,
webdriverDescriptor: Object.getOwnPropertyDescriptor(
Navigator.prototype,
'webdriver',
),
ua: navigator.userAgent,
uaCH: navigator.userAgentData?.toJSON(),
tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
webgl: (() => {
const c = document.createElement('canvas').getContext('webgl');
if (!c) return null;
const e = c.getExtension('WEBGL_debug_renderer_info');
return {
vendor: c.getParameter(e.UNMASKED_VENDOR_WEBGL),
renderer: c.getParameter(e.UNMASKED_RENDERER_WEBGL),
};
})(),
});
Six tells:
webdriver === true — launch flags leaking.
- UA contains
HeadlessChrome — running headless without persona.
Sec-CH-UA-* headers don't match userAgent string — UA-CH desync. Inspect block-req.md headers.
Intl.DateTimeFormat().resolvedOptions().timeZone doesn't match the proxy egress IP geo. Cross-check with https://ipinfo.io/json.
- WebGL renderer reports
SwiftShader or Google Inc. (Google).
- The cursor reaches its target in one frame.
3. CDP layer probe
() => {
const obj = {};
Object.defineProperty(obj, 'foo', {
get() { window.__leaked = true; return 'bar'; }
});
console.debug(obj);
return window.__leaked;
}" --world main
true indicates a Runtime.enable listener is attached. Targets that hand-roll their own probe (rather than relying on a vendor SDK) sometimes use this.
4. DOM layer diff
Compare polyfilled surface against real Chrome. Probe the polyfilled functions:
() => ({
chromeRuntime:
typeof chrome !== 'undefined' && typeof chrome.runtime !== 'undefined',
notif: Notification.permission,
perm: navigator.permissions ? 'present' : 'absent',
toStringFootprint: navigator.permissions?.query?.toString?.(),
});
Bot tells:
chromeRuntime: false on a non-extension context (real Chrome leaves this defined).
notif === 'denied' while Permissions.query({name:'notifications'}).state === 'prompt' — incoherence.
toStringFootprint not containing [native code] — polyfill toString not proxied.
5. Header diff
Read block-req.md (the headers we sent) and compare to a real browser request to the same URL captured separately. Common diffs:
- Header order. We can rewrite values via
Network.setExtraHTTPHeaders but cannot reorder at the wire level.
Sec-Fetch-* headers. These are generated by Chrome based on context. If they look wrong, navigation context is wrong (for instance, navigating cross-origin via a script vs a click).
Accept-Language. If empty or single-locale, persona is incomplete.
Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Platform. These come from UA-CH metadata — not from the UA string. If they say "Chromium" but the UA says Chrome, the persona was applied as a UA string only.
6. Behavioral layer
If you reached the page via a click on a search result, a CAPTCHA, or a "continue" button, the target may sample input timing.
Verify:
- Humanized input was on for the click that led here.
- The cursor path included
mousemove events between origin and target, not a teleport.
- Time between click and navigation isn't sub-100ms.
Humanized input is always on and cannot be disabled, so it is not a variable to rule
out here. Confirm instead that interactions went through the tools (click, fill,
type_text) rather than a hand-rolled evaluate_script dispatch, which bypasses the
humanized path entirely.
7. Network / TLS layer
If everything above passes, the block may be JA4 or HTTP/2 SETTINGS-frame correlation. We do not control this from MCP — Chrome speaks TLS. Likely causes:
- A proxy that terminates TLS and re-originates with its own JA4 (most authenticated forwarders do this). Switch to a pass-through proxy.
- A residential IP whose ASN is associated with cloud hosting.
Do not attempt to mitigate at the network layer from MCP. Document the constraint and route the run through different infrastructure.
8. Decide and act
Build a one-line summary per layer indicating pass / fail / unknown. The first failing layer is the next target.
| Layer | State | Next |
|---|
| CDP | pass | — |
| Launch | pass | — |
| DOM | fail (chrome.runtime polyfill toString leak) | Fix polyfill Function.prototype.toString proxy |
| Fingerprint | pass | — |
| Behavioral | unknown | Re-run with humanization confirmed-on |
| Network | pass (pass-through proxy) | — |
Hand to the relevant mitigation skill:
Tips
- Run steps 1 and 2 first. Cheap, fast, catch most cases.
- Steps 5 (header diff) is slow but high signal when a vendor SDK is involved.
- The block response often names the vendor.
cf-mitigated is Cloudflare; x-datadome is DataDome; Server: AkamaiGHost is Akamai; PerimeterX uses _px* cookies; Imperva uses incap_ses_*; Kasada uses x-kpsdk-* headers.
What NOT to do
- Do not iterate on polyfills before reading the block response. The vendor (Cloudflare vs DataDome vs Akamai) determines which signals matter.
- Do not retry the failed navigation in the same session repeatedly. A challenge cookie or fingerprint hash can pin the session as flagged.
- Do not change persona attributes mid-investigation. Hold the persona; change one mitigation at a time and re-test.