Skip to main content

The agent-native web engine. No Chromium. No Node. One Rust binary.

Project description

heso — The agent-native web engine. No Chromium. No Node. One Rust binary.

Site: heso.ca · Docs: heso.ca/docs · npm · PyPI · Releases

It fetches a URL, runs the JavaScript, lets you click, fill forms, search the web, and scrape many pages in parallel — and returns everything as JSON so an agent can use it.

binary       9.2 MB
cold start   ~80 ms   (open https://example.com, network included)
engine only  ~35 ms   (no network)
batch        ~1.1 s   for 8 URLs in parallel

heso agent demo

That's a real recording — Claude Code (claude -p from the repo root, with the heso skill loaded) discovering the verbs, navigating the page tree, and pulling the live top story off Hacker News. No edits, no replays.

Install

# Python (uv, pipx, or pip — any of them)
uv tool install heso          # or: pipx install heso  /  pip install heso

# Node
npm install -g @ixla/heso     # or one-shot: npx @ixla/heso open https://example.com

# Direct binary
# Windows:
powershell -c "irm https://github.com/blank3rs/heso/releases/latest/download/heso.zip -OutFile heso.zip; Expand-Archive heso.zip -DestinationPath ."

Currently shipping v0.0.1 Windows-x64 only. Linux + macOS binaries land with v0.0.2 (CI builds wiring up now). On other platforms, build from source for now.

After install, heso is on $PATH:

heso open https://example.com
# → { url, title, description, tree, actions, plat_hash, ... }

You get JSON: title, description, a heading tree, and a list of clickable elements numbered @e0, @e1, and so on.

A note before you read further

Most of this codebase was written with help from Claude under one person's direction. The co-author tag is on basically every commit. It moved fast, which means the feature surface ran ahead of real usage. Treat this as working code that needs more eyes on real workloads, not a finished product.

What it can do

Find and read things.

  • heso search "<query>" — searches the web (DuckDuckGo + Wikipedia, optional SearXNG). No API key.
  • heso open <url> — fetches and returns a page summary: title, headings, actionable elements.
  • heso read <url> — fetches, runs JS, returns the full picture: title, visible text, actions, forms, cookies, console output, framework detection. One call.
  • heso read <url> --complete — same, but heso loops "fire pending observers + click load-more + wait for DOM to settle" until the page stops changing. For lazy-loaded sites.
  • heso batch [open|read] <urls...> — runs many URLs in parallel. Shared cookie jar, JSON-Lines out.
  • heso wait <url> --selector-exists ".foo" (also --text-contains, --url-matches, --network-idle, --time) — blocks until a condition is true. No polling loop.

Interact with sites.

  • heso click <url> @e7 — click by element ref.
  • heso click <url> --text "Sign in" — or by visible text, CSS selector, or aria-label.
  • heso fill <url> @e3 "hello" — type into an input.
  • heso submit <url> @e9 — submit a form.
  • heso navigate — change URL within a session.
  • heso eval-dom <url> "<js>" — fetch, run scripts, then run your JS against the resulting DOM.

Recover from broken sites.

  • --best-effort on open / read / wait — exit 0 even when scripts crash. Output includes partial: true, partial_reason: "script_crash" | "wait_timeout" | "fetch_failed" | "parse_error", and failed_scripts: [...]. The agent sees what broke and decides what to try next.
  • --inject-script "<inline-js>" or --inject-script @file.js — run JS before the page's own scripts. Use it to shim a missing global (the canonical window.lunr cascade kind of thing).

Detect cross-call state changes.

  • heso read always returns a content_hash. Pass --since <prev_hash> to get a delta describing what changed (actions_added, actions_removed, forms_changed, text_changed, title_changed).

Stateful sessions.

  • heso serve — JSON-RPC over stdin/stdout. Cookies, DOM mutations, listeners, and history persist across calls. Useful for login → navigate → scrape flows.

What it can't do

  • No rendering. No canvas, WebGL, CSS layout, or video. If the meaning is in pixels, use a real browser.
  • CAPTCHAs and hard bot-detect. Hits one, stops. The default user-agent is Mozilla/5.0 (compatible; heso/0.0.1) so anything fingerprinting will see us coming.
  • Pages built on tech we don't simulate. Service Workers, WebRTC, WebUSB, WebBluetooth — not supported.
  • Sites whose JS we can't run. QuickJS isn't V8. Most works; some doesn't.
  • Sibling-script cascades we haven't shimmed. When script A sets window.X and script B reads it, and X doesn't exist on first load, heso surfaces the crash and the agent can --inject-script a stub.

Use as a library

The Python (heso) and Node (@ixla/heso) packages each ship two faces of the same bundled binary: a CLI on $PATH and a programmatic API that spawns that binary under the hood and gives you back parsed JSON as native objects. No FFI, no Python extension module, no N-API addon — subprocess + JSON is the contract.

# Python
import heso

page    = heso.open("https://example.com")              # -> dict
results = heso.search("rust web scraping", limit=5)     # -> dict
content = heso.read("https://example.com", complete=True)

# Stateful flow over one long-lived `heso serve` process:
with heso.session() as s:
    s.open("https://example.com")
    s.click(text="More information...")
    page = s.read()
// Node
import { open, search, read, session } from "@ixla/heso";

const page    = await open("https://example.com");
const results = await search("rust web scraping", { limit: 5 });
const content = await read("https://example.com", { complete: true });

await session(async (s) => {
  await s.open("https://example.com");
  await s.click({ text: "More information..." });
  const page = await s.read();
});

Per-language idioms: Python is snake_case + sync, Node is camelCase + Promises. Full API at heso.ca/docs.

Examples

Search the web, then read the top hits in parallel:

heso search "rust web scraping" --limit 5
heso batch read url1 url2 url3 --parallel 2

Read everything from one page in one call:

heso read https://nextjs.org/
# → { title, text, actions, forms, cookies, console, framework,
#     content_hash, lazy_hints, partial: false, ... }

Find by visible text, click, follow:

heso click https://news.ycombinator.com --text "More"

Wait for an SPA condition:

heso wait https://app.example.com/ --selector-exists ".dashboard" --timeout 5s

Rescue a broken site with a polyfill:

heso open https://shoelace.style --best-effort \
  --inject-script "window.lunr = (() => ({ Index: { load: () => ({}) } }))()"

Multi-step session over stdio:

heso serve
# → JSON-RPC. Page state, cookies, DOM all persist across requests.

Reproducibility (same seed → same output across machines):

heso eval-js --seed 42 'Math.random()'   # 0.5140492957650241
heso eval-js --seed 42 'Math.random()'   # 0.5140492957650241

Error handling

Both libraries throw a structured error (HesoError in Python, HesoError extends Error in Node) when the binary exits non-zero. Fields on the error tell you what to retry:

import heso
try:
    page = heso.read("https://shoelace.style")
except heso.HesoError as e:
    print(e.returncode, e.stderr[:200])  # exit code + first 200 chars of stderr
import { read, HesoError } from "@ixla/heso";
try {
  const page = await read("https://shoelace.style");
} catch (e) {
  if (e instanceof HesoError) {
    console.error(e.code, e.stderr.slice(0, 200));
  }
}

For sites that crash some scripts, use best_effort / bestEffort instead — heso exits 0 with a partial: true envelope so you handle the failure as data, not an exception:

page = heso.read("https://shoelace.style", best_effort=True)
if page["partial"]:
    print("got partial:", page["partial_reason"], page["failed_scripts"])

Plug into agent harnesses

heso is harness-agnostic. The same package serves five integration patterns:

Harness style How heso fits
Python frameworks (LangChain, Pydantic AI, LangGraph, smolagents, AgentScope) import heso. Each function returns a dict. Wrap with @tool / Tool(...) / a function schema.
Node / TS frameworks (Mastra, Vercel AI SDK, LangGraph.js, Stagehand, Browser Use TS) import { open, search } from "@ixla/heso". All async; TypeScript types ship in index.d.ts.
Skill-markdown harnesses (Claude Code, Cursor, Aider, Cline, Continue, Windsurf) Drop the manifest in the "Use as an agent skill" block below into ~/.claude/skills/heso/SKILL.md (or the harness's skills dir). The harness auto-discovers; heso on PATH does the rest.
CLI-spawning harnesses (Aider, shell-script agents, homegrown loops) Same heso <verb> ... CLI used by both libraries. JSON on stdout. No special integration.
Long-running JSON-RPC harnesses heso serve is a JSON-RPC 2.0 server over stdin/stdout. Cookies + DOM state persist across calls.

The verbs are the contract (see ADR 0017) — no heso-specific framework dependency, no adapter layer.

Use as an agent skill

heso is built to be a tool an agent calls, not a library a human drives. The cleanest integration is the skill markdown pattern that Claude Code, Cursor, Aider, Cline, and similar harnesses use:

---
name: heso
description: Use the heso headless browser (one Rust binary, no Chromium, no Node) to search the web, fetch pages, run their JavaScript, extract content, navigate, fill forms, or click links. Prefer this over WebFetch when you need a DOM, stateful clicks, or framework-rendered content.
---

## Verbs

- `heso search "<query>" [--limit N]` — web search via DDG + Wikipedia
- `heso open <url>` — page summary
- `heso read <url> [--complete]` — full content + actions + forms (use --complete for lazy-loaded sites)
- `heso wait <url> --selector-exists ".x"` — block until a condition is true
- `heso batch [open|read] <urls...> [--parallel N]` — parallel scrape
- `heso click <url> --text "..." | --selector "..." | @eN` — click
- `heso fill <url> @eN "value"` — type into input
- `heso submit <url> @eN` — submit form
- `heso eval-dom <url> "<js>"` — run JS against the page
- `heso serve` — multi-step JSON-RPC session
- `--best-effort` on open/read/wait — exit 0 on partial failures, surface what broke
- `--inject-script "<js>" | @file` — inject a polyfill before page scripts run

The verbs are the contract. Same shape works in any harness that does tool or skill markdown.

Stats

Measured on Windows 11, AMD x86_64, with the release binary:

Thing Number
Binary size 9.2 MB
Cold start (open https://example.com, network included) ~80 ms
Engine-only (no network, local fixture) ~35 ms
Batch (8 URLs, --parallel 8) ~1.1 s total
Search (DDG, 5 results) ~1 s

No comparisons to other tools — different tools have different tradeoffs and "X is faster than Y" framing rarely survives contact with a real workload.

Building from source

If you're on Linux/macOS today (v0.0.2 will ship prebuilt binaries) or want to hack on heso itself:

git clone https://github.com/blank3rs/heso
cd heso
cargo build --release -p heso-cli
./target/release/heso search "rust web scraping" --limit 5

Requires Rust 1.80+ (rustup from https://rustup.rs).

Status

Pre-alpha. v0.0.1 is on every registry. Worth trying if the use case fits; not worth depending on in production yet. Next (v0.0.2) ships Linux + macOS binaries and the library APIs above.

License

MIT or Apache-2.0, your choice.


Full docs: heso.ca/docs · Site: heso.ca · npm: @ixla/heso · PyPI: heso

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

heso-0.0.3-py3-none-win_amd64.whl (4.2 MB view details)

Uploaded Python 3Windows x86-64

heso-0.0.3-py3-none-manylinux_2_17_x86_64.whl (4.5 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

heso-0.0.3-py3-none-manylinux_2_17_aarch64.whl (4.1 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64

heso-0.0.3-py3-none-macosx_11_0_x86_64.whl (4.3 MB view details)

Uploaded Python 3macOS 11.0+ x86-64

heso-0.0.3-py3-none-macosx_11_0_arm64.whl (3.9 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

File details

Details for the file heso-0.0.3-py3-none-win_amd64.whl.

File metadata

  • Download URL: heso-0.0.3-py3-none-win_amd64.whl
  • Upload date:
  • Size: 4.2 MB
  • Tags: Python 3, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for heso-0.0.3-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 789c65b9efb4b1685df1144020a02a67008d4f6525604a18c190014de2fc951c
MD5 399930fd9be70b55dd03503c8a2979bf
BLAKE2b-256 decbca0fda1281bfd124e5eb3e532df60185f06f87ed21ff2b4ea8e12088ee77

See more details on using hashes here.

File details

Details for the file heso-0.0.3-py3-none-manylinux_2_17_x86_64.whl.

File metadata

  • Download URL: heso-0.0.3-py3-none-manylinux_2_17_x86_64.whl
  • Upload date:
  • Size: 4.5 MB
  • Tags: Python 3, manylinux: glibc 2.17+ x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for heso-0.0.3-py3-none-manylinux_2_17_x86_64.whl
Algorithm Hash digest
SHA256 b1d3e7a710bf7aeffba0fe1b0a5e9f87d4b14f2985aaf97f17cc18c7ee5caada
MD5 de9c7d4ee7b13452d57ba5080d58afbc
BLAKE2b-256 3562e0f4bcd429eb221b7cc50e0214357f48a9c3e224cc48f3d83dd65e446c81

See more details on using hashes here.

File details

Details for the file heso-0.0.3-py3-none-manylinux_2_17_aarch64.whl.

File metadata

File hashes

Hashes for heso-0.0.3-py3-none-manylinux_2_17_aarch64.whl
Algorithm Hash digest
SHA256 ee8154e6146bcf910c568a8b3e49bde5593923e4e4075842bde33d3416b63ccb
MD5 ac4603340ab3036020d71ceac7265a26
BLAKE2b-256 e16eb8eb99bafe3a3936b4ea27a3df14ca3fc6c093481f5e67438ed959a9e9d6

See more details on using hashes here.

File details

Details for the file heso-0.0.3-py3-none-macosx_11_0_x86_64.whl.

File metadata

  • Download URL: heso-0.0.3-py3-none-macosx_11_0_x86_64.whl
  • Upload date:
  • Size: 4.3 MB
  • Tags: Python 3, macOS 11.0+ x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for heso-0.0.3-py3-none-macosx_11_0_x86_64.whl
Algorithm Hash digest
SHA256 018ee07f4c801cd4364ebdb6d8e8f6fa325d23dcfdb4d0048b6fd5f546145a73
MD5 40fb4880fd7ee2b354bca49b544ee7b0
BLAKE2b-256 9308dcfdc852b5e8453c98fe5dd0a3b33e096ed104e6b7c5464e55e5af79c953

See more details on using hashes here.

File details

Details for the file heso-0.0.3-py3-none-macosx_11_0_arm64.whl.

File metadata

  • Download URL: heso-0.0.3-py3-none-macosx_11_0_arm64.whl
  • Upload date:
  • Size: 3.9 MB
  • Tags: Python 3, macOS 11.0+ ARM64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for heso-0.0.3-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 4714ac559a8c2e112dcd22b8cec78ab3e421064cbb3d80347ce340d724b57e65
MD5 ead293562b5c64f18ba8cc2400b2c575
BLAKE2b-256 942a70ac99d40f7d6d50179c6e8f3f2f4f02e01bb2448e964d5a6c4bf607b827

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page