URL to Screenshot
Capture an arbitrary http(s) URL to a clean, verified PNG (viewport or full
page) across Linux, macOS, and Windows. The flow detects an installed browser,
admits the URL through a fail-closed SSRF gate, captures over headless Chromium,
dismisses cookie-consent overlays, detects blank output, and ends with an
explicit verify gate that is the only thing allowed to declare success.
This is the repo's first skill that drives a real browser against an
attacker-influenceable URL, so the SSRF, sandbox, and timeout safeguards are
first-class and the security posture is documented honestly below.
Windows Runtime Commands
On native Windows, use the managed Windows runner and the native runtime command
target. Set $runtime to the installed runtime root. Multi-agent installs usually
use %LOCALAPPDATA%\ai-agents-skills\runtime.
$runtime = if ($env:AAS_RUNTIME_ROOT) { $env:AAS_RUNTIME_ROOT } else { "$env:LOCALAPPDATA\ai-agents-skills\runtime" }
& "$runtime\run_skill.ps1" "skills/url-to-screenshot-runtime/run_url_to_screenshot.ps1" doctor
& "$runtime\run_skill.ps1" "skills/url-to-screenshot-runtime/run_url_to_screenshot.ps1" doctor
POSIX examples below use run_skill.sh and the .sh command target.
When to use
Use this skill when the user wants to:
- screenshot a public web page for documentation, research, or QA
- capture a full-page (beyond-viewport) image, not just the visible viewport
- capture at a specific viewport size or device-scale factor
- dismiss a cookie-consent overlay that occludes the content
Do NOT use this to bypass paywalls, age gates, or login walls, or to capture
content the agent's own policy would refuse. Consent dismissal is scoped to
cookie-consent overlays only.
Runtime helper (verbs)
The executable engine ships as the url-to-screenshot-runtime skill. Run it via
the managed runner:
bash "${AAS_RUNTIME_ROOT:-$HOME/.local/share/ai-agents-skills/runtime}/run_skill.sh" skills/url-to-screenshot-runtime/run_url_to_screenshot.sh doctor
bash "${AAS_RUNTIME_ROOT:-$HOME/.local/share/ai-agents-skills/runtime}/run_skill.sh" skills/url-to-screenshot-runtime/run_url_to_screenshot.sh capture --url https://example.com/ --out shot.png
bash "${AAS_RUNTIME_ROOT:-$HOME/.local/share/ai-agents-skills/runtime}/run_skill.sh" skills/url-to-screenshot-runtime/run_url_to_screenshot.sh verify --png shot.png --expected-width 1280 --expected-height 800
Verbs: doctor (readiness), capture (URL -> PNG, SSRF-gated), verify (the
artifact-truth gate), selftest (offline smoke).
Key options for capture: --url (required, http/https only), --out,
--viewport WxH (or --width/--height), --full-page, --device-scale,
--wait, --timeout, --consent on|off, --engine auto|oneshot|cdp,
--browser, --allow-private-targets (relaxes the private-IP block ONLY), and
--allow-file-urls (trusted local fixtures/testing ONLY), and
--same-origin-only (CDP-only strict initial-origin boundary) — see Security
notes.
Required workflow
- Run
doctor first to confirm a browser is present. file-exists and
offline-smoke verification passing does NOT imply a browser is installed --
only doctor reports real capture readiness.
- Run
capture with the target URL. The URL is admitted through the SSRF gate
before any browser launch; a blocked URL yields a BLOCKED_* verdict.
- Run
verify on the produced PNG. Treat the capture as done only when
verify returns final_verdict=VERIFIED.
Strict approval / verification surface
capture produces a PNG but never declares success. The verify verb is the
only thing that declares a real screenshot done: final_verdict=VERIFIED only
when file/decode/dimensions/not-blank/consent all PASS. "The file exists",
source inspection, or "Chromium exited 0" never constitute final success. Use
BLOCKED_* / UNVERIFIED wording for any non-VERIFIED state; do not use
approval-style wording for a capture that did not verify.
Security notes
This skill fetches an attacker-influenceable host and drives a sandbox-sensitive
browser, so the security posture is stated plainly:
- SSRF admission gate (fail-closed, pre-navigation).
validate_target_url
enforces a scheme allow-list (http/https only), resolves every A/AAAA and
rejects loopback/private/link-local/reserved/multicast addresses, and applies
an UNCONDITIONAL cloud-metadata denylist (169.254.169.254,
metadata.google.internal, and peers). It is an admission decision only; it
cannot bind Chromium's own resolver, redirects, or sub-resource fetches.
- Tier scope. With the default
--consent on, ordinary captures enter
Tier-2 (CDP) because consent dismissal is a CDP DOM operation; Tier-1
one-shot is the --consent off fallback. In default Tier-1 the only
protections are the Python pre-resolve admission gate plus a single
--host-resolver-rules MAP pin of the validated initial host, so
redirect/sub-resource SSRF is unguarded there (in-scope-and-unmitigated). The
PRIMARY browser-side control is Tier-2 CDP Fetch-domain request
interception: Fetch is enabled (catch-all, request stage) BEFORE
navigation, so every request — main frame, redirects, and sub-resources /
JS-initiated fetches — is PAUSED and re-validated (scheme allow-list plus a
fresh resolve-and-check of every resolved IP) and FAILED BEFORE SEND on a
violation (Fetch.failRequest), never merely observed after the fact. v1
policy: any private/metadata hit aborts the whole capture with the matching
BLOCKED_* status; redirects are capped. The --host-resolver-rules pin
remains as same-host-rebind defense-in-depth.
- CDP origin posture. The CDP endpoint launches bound to
127.0.0.1 on an
ephemeral port with NO --remote-allow-origins flag at all, and the stdlib
client sends NO Origin header, so Chromium's default-deny of Origin-bearing
CDP applies. The real protections are this default-deny, the per-target
webSocketDebuggerUrl GUID plus the loopback bind, and finally teardown. On
a shared host, any local process can read the CDP endpoint and per-target GUID
from loopback /json during the capture window; the GUID is a loopback handle,
not a true secret.
- Override scope.
--allow-private-targets relaxes the private/loopback/
link-local block ONLY. It NEVER relaxes the scheme allow-list and NEVER the
cloud-metadata denylist. It requires the CLI flag; the env var
URL_TO_SCREENSHOT_ALLOW_PRIVATE=1 alone does not enable it, so an inherited
or poisoned environment cannot silently disable SSRF blocking.
--allow-file-urls (trusted fixtures only). Off by default; without it,
file:/data:/etc. stay BLOCKED_SCHEME. When set it adds file: to the
scheme allow-list so the engine can capture trusted local HTML fixtures (the CI
capture job uses it against u2s/htmlfixtures/*.html). It enables LOCAL FILE
READS (e.g. file:///etc/passwd), so it is for trusted local fixtures/testing
ONLY and must NEVER be used on attacker-influenceable input. Like
--allow-private-targets it requires the CLI flag; the environment alone never
enables it. A file: URL has no remote host, so the SSRF IP checks do not
apply to it.
- Strict origin boundary.
--same-origin-only forces CDP and aborts every
paused redirect or sub-resource whose origin tuple differs from the initial
request: scheme, canonical hostname, or effective port (explicit port,
otherwise 80 for HTTP and 443 for HTTPS). Auto fallback to Tier-1 is disabled
while the flag is active, and explicit Tier-1 is rejected.
- Residual limitation for generic cross-host capture. In ordinary Tier-2
capture, an allowed request to another host is re-resolved and checked by the
Python CDP interceptor before release, but Chromium resolves that cross-host
request independently. The checked address can therefore differ from the
address Chromium ultimately connects to: a cross-host DNS TOCTOU window is
narrowed, not eliminated. Tier-1 one-shot has no per-request hook. The strict
same-origin venue-proof path does not admit a different host and pins the
initial host, so this particular cross-host residual is not a venue-proof
limitation. This skill does not imply full SSRF protection for generic capture.
References
references/engine-and-cdp.md — CDP websocket (no --remote-allow-origins
flag, no client Origin header), per-target GUID loopback handle, consent DOM
removal, one-shot fallback, CDP Fetch-domain request interception (blocks
before send, all hosts).
references/browsers-and-platforms.md — browser detection order and per-OS
notes.
references/verification-gates.md — blank-output detection, render-wait /
timeout semantics, the strict verify gate, and SSRF admission-vs-navigation
scope.
Boundaries
- The executable engine ships as the
url-to-screenshot-runtime skill; this
skill-file is the user-facing workflow and references.
- On openclaw, NEITHER skill installs natively: this skill-file ships
references/ files (a non-SKILL.md payload) so the openclaw skill-file
install is blocked, and openclaw runtime support is manual/fake-root. Neither
runs on openclaw real-system until an approved runtime manifest and broker
exist.
file-exists / offline-smoke verification passing does NOT imply a browser
is present. Real capture readiness is reported only by doctor.
- The real native browser capture / CDP / timeout-reap tier on Windows and macOS
is not exercised by automated CI; it requires a manual
doctor + capture run.
Windows job-object/taskkill reaping and locked-file profile cleanup are
verified only by manual Windows runs.
1---2name: url-to-screenshot3description: Use when the user wants to capture a web page (an http or https URL) to a clean PNG screenshot, in viewport or full-page mode, with cookie-consent dismissal, timeouts, SSRF-safe URL admission, and blank-output verification, across Linux, macOS, and Windows. The executable engine ships as the url-to-screenshot-runtime skill.4---56# URL to Screenshot78Capture an arbitrary `http(s)` URL to a clean, verified PNG (viewport or full9page) across Linux, macOS, and Windows. The flow detects an installed browser,10admits the URL through a fail-closed SSRF gate, captures over headless Chromium,11dismisses cookie-consent overlays, detects blank output, and ends with an12explicit `verify` gate that is the only thing allowed to declare success.1314This is the repo's first skill that drives a real browser against an15attacker-influenceable URL, so the SSRF, sandbox, and timeout safeguards are16first-class and the security posture is documented honestly below.1718## Windows Runtime Commands1920On native Windows, use the managed Windows runner and the native runtime command21target. Set `$runtime` to the installed runtime root. Multi-agent installs usually22use `%LOCALAPPDATA%\ai-agents-skills\runtime`.2324```powershell25$runtime = if ($env:AAS_RUNTIME_ROOT) { $env:AAS_RUNTIME_ROOT } else { "$env:LOCALAPPDATA\ai-agents-skills\runtime" }26& "$runtime\run_skill.ps1" "skills/url-to-screenshot-runtime/run_url_to_screenshot.ps1" doctor27& "$runtime\run_skill.ps1" "skills/url-to-screenshot-runtime/run_url_to_screenshot.ps1" doctor28```2930POSIX examples below use `run_skill.sh` and the `.sh` command target.3132## When to use3334Use this skill when the user wants to:3536- screenshot a public web page for documentation, research, or QA37- capture a full-page (beyond-viewport) image, not just the visible viewport38- capture at a specific viewport size or device-scale factor39- dismiss a cookie-consent overlay that occludes the content4041Do NOT use this to bypass paywalls, age gates, or login walls, or to capture42content the agent's own policy would refuse. Consent dismissal is scoped to43cookie-consent overlays only.4445## Runtime helper (verbs)4647The executable engine ships as the `url-to-screenshot-runtime` skill. Run it via48the managed runner:4950```bash51bash "${AAS_RUNTIME_ROOT:-$HOME/.local/share/ai-agents-skills/runtime}/run_skill.sh" skills/url-to-screenshot-runtime/run_url_to_screenshot.sh doctor52bash "${AAS_RUNTIME_ROOT:-$HOME/.local/share/ai-agents-skills/runtime}/run_skill.sh" skills/url-to-screenshot-runtime/run_url_to_screenshot.sh capture --url https://example.com/ --out shot.png53bash "${AAS_RUNTIME_ROOT:-$HOME/.local/share/ai-agents-skills/runtime}/run_skill.sh" skills/url-to-screenshot-runtime/run_url_to_screenshot.sh verify --png shot.png --expected-width 1280 --expected-height 80054```5556Verbs: `doctor` (readiness), `capture` (URL -> PNG, SSRF-gated), `verify` (the57artifact-truth gate), `selftest` (offline smoke).5859Key options for `capture`: `--url` (required, http/https only), `--out`,60`--viewport WxH` (or `--width`/`--height`), `--full-page`, `--device-scale`,61`--wait`, `--timeout`, `--consent on|off`, `--engine auto|oneshot|cdp`,62`--browser`, `--allow-private-targets` (relaxes the private-IP block ONLY), and63`--allow-file-urls` (trusted local fixtures/testing ONLY), and64`--same-origin-only` (CDP-only strict initial-origin boundary) — see Security65notes.6667## Required workflow68691. Run `doctor` first to confirm a browser is present. `file-exists` and70 `offline-smoke` verification passing does NOT imply a browser is installed --71 only `doctor` reports real capture readiness.722. Run `capture` with the target URL. The URL is admitted through the SSRF gate73 before any browser launch; a blocked URL yields a `BLOCKED_*` verdict.743. Run `verify` on the produced PNG. Treat the capture as done only when75 `verify` returns `final_verdict=VERIFIED`.7677## Strict approval / verification surface7879`capture` produces a PNG but never declares success. The `verify` verb is the80only thing that declares a real screenshot done: `final_verdict=VERIFIED` only81when file/decode/dimensions/not-blank/consent all PASS. "The file exists",82source inspection, or "Chromium exited 0" never constitute final success. Use83`BLOCKED_*` / `UNVERIFIED` wording for any non-VERIFIED state; do not use84approval-style wording for a capture that did not verify.8586## Security notes8788This skill fetches an attacker-influenceable host and drives a sandbox-sensitive89browser, so the security posture is stated plainly:9091- **SSRF admission gate (fail-closed, pre-navigation).** `validate_target_url`92 enforces a scheme allow-list (`http`/`https` only), resolves every A/AAAA and93 rejects loopback/private/link-local/reserved/multicast addresses, and applies94 an UNCONDITIONAL cloud-metadata denylist (`169.254.169.254`,95 `metadata.google.internal`, and peers). It is an admission decision only; it96 cannot bind Chromium's own resolver, redirects, or sub-resource fetches.97- **Tier scope.** With the default `--consent on`, ordinary captures enter98 Tier-2 (CDP) because consent dismissal is a CDP DOM operation; Tier-199 one-shot is the `--consent off` fallback. In default Tier-1 the only100 protections are the Python pre-resolve admission gate plus a single101 `--host-resolver-rules` MAP pin of the validated initial host, so102 redirect/sub-resource SSRF is unguarded there (in-scope-and-unmitigated). The103 PRIMARY browser-side control is Tier-2 CDP **`Fetch`-domain request104 interception**: `Fetch` is enabled (catch-all, request stage) BEFORE105 navigation, so every request — main frame, redirects, and sub-resources /106 JS-initiated fetches — is PAUSED and re-validated (scheme allow-list plus a107 fresh resolve-and-check of every resolved IP) and FAILED BEFORE SEND on a108 violation (`Fetch.failRequest`), never merely observed after the fact. v1109 policy: any private/metadata hit aborts the whole capture with the matching110 `BLOCKED_*` status; redirects are capped. The `--host-resolver-rules` pin111 remains as same-host-rebind defense-in-depth.112- **CDP origin posture.** The CDP endpoint launches bound to `127.0.0.1` on an113 ephemeral port with NO `--remote-allow-origins` flag at all, and the stdlib114 client sends NO `Origin` header, so Chromium's default-deny of Origin-bearing115 CDP applies. The real protections are this default-deny, the per-target116 `webSocketDebuggerUrl` GUID plus the loopback bind, and `finally` teardown. On117 a shared host, any local process can read the CDP endpoint and per-target GUID118 from loopback `/json` during the capture window; the GUID is a loopback handle,119 not a true secret.120- **Override scope.** `--allow-private-targets` relaxes the private/loopback/121 link-local block ONLY. It NEVER relaxes the scheme allow-list and NEVER the122 cloud-metadata denylist. It requires the CLI flag; the env var123 `URL_TO_SCREENSHOT_ALLOW_PRIVATE=1` alone does not enable it, so an inherited124 or poisoned environment cannot silently disable SSRF blocking.125- **`--allow-file-urls` (trusted fixtures only).** Off by default; without it,126 `file:`/`data:`/etc. stay `BLOCKED_SCHEME`. When set it adds `file:` to the127 scheme allow-list so the engine can capture trusted local HTML fixtures (the CI128 capture job uses it against `u2s/htmlfixtures/*.html`). It enables LOCAL FILE129 READS (e.g. `file:///etc/passwd`), so it is for trusted local fixtures/testing130 ONLY and must NEVER be used on attacker-influenceable input. Like131 `--allow-private-targets` it requires the CLI flag; the environment alone never132 enables it. A `file:` URL has no remote host, so the SSRF IP checks do not133 apply to it.134- **Strict origin boundary.** `--same-origin-only` forces CDP and aborts every135 paused redirect or sub-resource whose origin tuple differs from the initial136 request: scheme, canonical hostname, or effective port (explicit port,137 otherwise 80 for HTTP and 443 for HTTPS). Auto fallback to Tier-1 is disabled138 while the flag is active, and explicit Tier-1 is rejected.139- **Residual limitation for generic cross-host capture.** In ordinary Tier-2140 capture, an allowed request to another host is re-resolved and checked by the141 Python CDP interceptor before release, but Chromium resolves that cross-host142 request independently. The checked address can therefore differ from the143 address Chromium ultimately connects to: a cross-host DNS TOCTOU window is144 narrowed, not eliminated. Tier-1 one-shot has no per-request hook. The strict145 same-origin venue-proof path does not admit a different host and pins the146 initial host, so this particular cross-host residual is not a venue-proof147 limitation. This skill does not imply full SSRF protection for generic capture.148149## References150151- `references/engine-and-cdp.md` — CDP websocket (no `--remote-allow-origins`152 flag, no client `Origin` header), per-target GUID loopback handle, consent DOM153 removal, one-shot fallback, CDP `Fetch`-domain request interception (blocks154 before send, all hosts).155- `references/browsers-and-platforms.md` — browser detection order and per-OS156 notes.157- `references/verification-gates.md` — blank-output detection, render-wait /158 timeout semantics, the strict `verify` gate, and SSRF admission-vs-navigation159 scope.160161## Boundaries162163- The executable engine ships as the `url-to-screenshot-runtime` skill; this164 skill-file is the user-facing workflow and references.165- On openclaw, NEITHER skill installs natively: this skill-file ships166 `references/` files (a non-`SKILL.md` payload) so the openclaw skill-file167 install is blocked, and openclaw runtime support is manual/fake-root. Neither168 runs on openclaw real-system until an approved runtime manifest and broker169 exist.170- `file-exists` / `offline-smoke` verification passing does NOT imply a browser171 is present. Real capture readiness is reported only by `doctor`.172- The real native browser capture / CDP / timeout-reap tier on Windows and macOS173 is not exercised by automated CI; it requires a manual `doctor` + capture run.174 Windows job-object/`taskkill` reaping and locked-file profile cleanup are175 verified only by manual Windows runs.