Lightweight headless WebKit browser designed for agents first
Swift
4
98 commits
updated Sep 16, 2026
A command-line tool that opens web pages and tells computer programs what it sees.
SleepyHollow is a headless browser built for AI agents and automated tests, not people. It renders real pages with the WebKit engine already built into every Mac, then lets you ask for what you need — a screenshot, the raw HTML, the accessibility tree, a log of network requests — as one command that prints structured text and exits with a code that means something. No browser profile, no background process, no protocol to speak: run a verb, read the output, move on.
SleepyHollow is Swift and needs macOS 12 or later — it links the system
WebKit.framework on purpose. There's no Homebrew formula yet, so build it
from source:
git clone https://github.com/bensyverson/sleepyhollow.git
cd sleepyhollow
swift package experimental-install --product sleepy
That builds a release binary and copies it to ~/.swiftpm/bin/sleepy. Add
that directory to your PATH once (export PATH="$HOME/.swiftpm/bin:$PATH"
in ~/.zshrc). The installer refuses to overwrite, so upgrading (or removing)
goes through swift package experimental-uninstall sleepy first.
To build without installing, run swift build -c release instead — the
binary lands at .build/release/sleepy, and you can put it on your PATH or
call it by that full path.
To use the library from another Swift package instead, add it as a
dependency in Package.swift:
.package(url: "https://github.com/bensyverson/sleepyhollow.git", branch: "main")
(Pin to a version once 1.0 is tagged.)
Load a page and check that it's alive:
$ sleepy load https://example.com/
{
"consoleErrorCount" : 0,
"dialogs" : [],
"finalURL" : "https://example.com/",
"httpStatus" : 200
}
Read the page the way assistive technology would — roles, names, and states, with layout stripped away:
$ sleepy ax https://example.com/
document "Example Domain"
heading "Example Domain" (level=1)
paragraph
text "This domain is for use in documentation examples without needing permission. Avoid use in operations."
paragraph
link "Learn more"
Take a screenshot:
sleepy shot https://example.com/ --out shot.png
Assert the page reads and fits — each exits 1 when it finds something, so it drops straight into a script:
sleepy contrast https://example.com/ --min wcag-aa
sleepy overflow https://example.com/ --size 390x800
If a call fails and you don't know why — a load that never finishes under a sandbox, a session that won't open — ask the tool itself:
sleepy doctor
Every loading verb (load, shot, pdf, archive, dom, query, style,
find, ax, contrast, overflow, console, wire, eval) takes the same shape — a URL, then
flags for viewport size, theme, waiting, and one-shot actions like
--click/--fill/--submit. Run sleepy with no arguments for a primer, or
sleepy <verb> --help for that verb's flags and examples. Exit codes are a
public contract (0 success, 1 clean negative, 2 usage error, 3 timeout, 4 load
failure, 5 environment error) — see Sources/SleepyHollow/Core/ExitStatus.swift
for the full table.
Need a page to outlive one invocation? sleepy open <url> --name <n> starts a
named session; every verb then takes --session <n> to act on that same live
page, sleepy sessions list shows what's open, and sleepy close <n> ends
it. Without --session, every invocation loads a fresh, ephemeral page unless
you attach a --jar for cookies that should outlive it.
PageHost is the same headless browser the CLI drives, usable directly from
Swift. Two of its defaults are worth knowing if you're coming from a raw
WKWebView: a host never follows the Mac's Dark Mode setting (see
ColorTheme), and a pinned --scale/ShotScale refuses to upsample, so it
fails loudly on a non-Retina host instead of quietly rendering soft.
import SleepyHollow
let host = PageHost(options: LoadOptions(size: ViewportSize(width: 1280, height: 800)))
let facts = try await host.load(url)
let shot = try await ShotOperation(scale: try ShotScale(factor: 2)).execute(on: host)
Hosts that need one identity — sign in once, then render many pages — are
built as members of a HostGroup, which owns the cookie store and the jar
for all of them. It does not share an HTTP cache: see
project/2026-08-29-host-group-cache.md
for what WebKit does and does not share.
Build the API reference with swift package generate-documentation (see
CLAUDE.md for the full DocC workflow).
peep compare as the
baseline-comparison path for a visual regression. The rule that decides
which tool owns a feature: anything that needs the page or page
coordinates is SleepyHollow's; anything that works on an arbitrary PNG is
PixelPeeper's.sleepy recipes / sleepy help recipes)swift package generate-documentation, or browse it live with
swift package preview-documentationBuilt by Ben Syverson. Released under the MIT License.
98 commits
Swift
96.2%
HTML
3.2%
Lightweight headless WebKit browser designed for agents first
Swift
4
98 commits
updated Sep 16, 2026
A command-line tool that opens web pages and tells computer programs what it sees.
SleepyHollow is a headless browser built for AI agents and automated tests, not people. It renders real pages with the WebKit engine already built into every Mac, then lets you ask for what you need — a screenshot, the raw HTML, the accessibility tree, a log of network requests — as one command that prints structured text and exits with a code that means something. No browser profile, no background process, no protocol to speak: run a verb, read the output, move on.
SleepyHollow is Swift and needs macOS 12 or later — it links the system
WebKit.framework on purpose. There's no Homebrew formula yet, so build it
from source:
git clone https://github.com/bensyverson/sleepyhollow.git
cd sleepyhollow
swift package experimental-install --product sleepy
That builds a release binary and copies it to ~/.swiftpm/bin/sleepy. Add
that directory to your PATH once (export PATH="$HOME/.swiftpm/bin:$PATH"
in ~/.zshrc). The installer refuses to overwrite, so upgrading (or removing)
goes through swift package experimental-uninstall sleepy first.
To build without installing, run swift build -c release instead — the
binary lands at .build/release/sleepy, and you can put it on your PATH or
call it by that full path.
To use the library from another Swift package instead, add it as a
dependency in Package.swift:
.package(url: "https://github.com/bensyverson/sleepyhollow.git", branch: "main")
(Pin to a version once 1.0 is tagged.)
Load a page and check that it's alive:
$ sleepy load https://example.com/
{
"consoleErrorCount" : 0,
"dialogs" : [],
"finalURL" : "https://example.com/",
"httpStatus" : 200
}
Read the page the way assistive technology would — roles, names, and states, with layout stripped away:
$ sleepy ax https://example.com/
document "Example Domain"
heading "Example Domain" (level=1)
paragraph
text "This domain is for use in documentation examples without needing permission. Avoid use in operations."
paragraph
link "Learn more"
Take a screenshot:
sleepy shot https://example.com/ --out shot.png
Assert the page reads and fits — each exits 1 when it finds something, so it drops straight into a script:
sleepy contrast https://example.com/ --min wcag-aa
sleepy overflow https://example.com/ --size 390x800
If a call fails and you don't know why — a load that never finishes under a sandbox, a session that won't open — ask the tool itself:
sleepy doctor
Every loading verb (load, shot, pdf, archive, dom, query, style,
find, ax, contrast, overflow, console, wire, eval) takes the same shape — a URL, then
flags for viewport size, theme, waiting, and one-shot actions like
--click/--fill/--submit. Run sleepy with no arguments for a primer, or
sleepy <verb> --help for that verb's flags and examples. Exit codes are a
public contract (0 success, 1 clean negative, 2 usage error, 3 timeout, 4 load
failure, 5 environment error) — see Sources/SleepyHollow/Core/ExitStatus.swift
for the full table.
Need a page to outlive one invocation? sleepy open <url> --name <n> starts a
named session; every verb then takes --session <n> to act on that same live
page, sleepy sessions list shows what's open, and sleepy close <n> ends
it. Without --session, every invocation loads a fresh, ephemeral page unless
you attach a --jar for cookies that should outlive it.
PageHost is the same headless browser the CLI drives, usable directly from
Swift. Two of its defaults are worth knowing if you're coming from a raw
WKWebView: a host never follows the Mac's Dark Mode setting (see
ColorTheme), and a pinned --scale/ShotScale refuses to upsample, so it
fails loudly on a non-Retina host instead of quietly rendering soft.
import SleepyHollow
let host = PageHost(options: LoadOptions(size: ViewportSize(width: 1280, height: 800)))
let facts = try await host.load(url)
let shot = try await ShotOperation(scale: try ShotScale(factor: 2)).execute(on: host)
Hosts that need one identity — sign in once, then render many pages — are
built as members of a HostGroup, which owns the cookie store and the jar
for all of them. It does not share an HTTP cache: see
project/2026-08-29-host-group-cache.md
for what WebKit does and does not share.
Build the API reference with swift package generate-documentation (see
CLAUDE.md for the full DocC workflow).
peep compare as the
baseline-comparison path for a visual regression. The rule that decides
which tool owns a feature: anything that needs the page or page
coordinates is SleepyHollow's; anything that works on an arbitrary PNG is
PixelPeeper's.sleepy recipes / sleepy help recipes)swift package generate-documentation, or browse it live with
swift package preview-documentationBuilt by Ben Syverson. Released under the MIT License.
98 commits
Swift
96.2%
HTML
3.2%