npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@askjo/camofox-browser

v1.3.1

Published

Headless browser automation server and OpenClaw plugin for AI agents - anti-detection, element refs, and session isolation

Readme


Why

AI agents need to browse the real web. Playwright gets blocked. Headless Chrome gets fingerprinted. Stealth plugins become the fingerprint.

Camoufox patches Firefox at the C++ implementation level - navigator.hardwareConcurrency, WebGL renderers, AudioContext, screen geometry, WebRTC - all spoofed before JavaScript ever sees them. No shims, no wrappers, no tells.

This project wraps that engine in a REST API built for agents: accessibility snapshots instead of bloated HTML, stable element refs for clicking, and search macros for common sites.

Features

  • C++ Anti-Detection - bypasses Google, Cloudflare, and most bot detection
  • Element Refs - stable e1, e2, e3 identifiers for reliable interaction
  • Token-Efficient - accessibility snapshots are ~90% smaller than raw HTML
  • Runs on Anything - lazy browser launch + idle shutdown keeps memory at ~40MB when idle. Designed to share a box with the rest of your stack — Raspberry Pi, $5 VPS, shared Railway infra.
  • Session Isolation - separate cookies/storage per user
  • Cookie Import - inject Netscape-format cookie files for authenticated browsing
  • Proxy + GeoIP - route traffic through residential proxies with automatic locale/timezone
  • Structured Logging - JSON log lines with request IDs for production observability
  • YouTube Transcripts - extract captions from any YouTube video via yt-dlp, no API key needed
  • Search Macros - @google_search, @youtube_search, @amazon_search, @reddit_subreddit, and 10 more
  • Snapshot Screenshots - include a base64 PNG screenshot alongside the accessibility snapshot
  • Large Page Handling - automatic snapshot truncation with offset-based pagination
  • Deploy Anywhere - Docker, Fly.io, Railway

Optional Dependencies

| Dependency | Purpose | Install | |-----------|---------|---------| | yt-dlp | YouTube transcript extraction (fast path) | pip install yt-dlp or brew install yt-dlp |

The Docker image includes yt-dlp. For local dev, install it for the /youtube/transcript endpoint. Without it, the endpoint falls back to a slower browser-based method.

Quick Start

OpenClaw Plugin

openclaw plugins install @askjo/camofox-browser

Tools: camofox_create_tab · camofox_snapshot · camofox_click · camofox_type · camofox_navigate · camofox_scroll · camofox_screenshot · camofox_close_tab · camofox_list_tabs · camofox_import_cookies

Standalone

git clone https://github.com/jo-inc/camofox-browser
cd camofox-browser
npm install
npm start  # downloads Camoufox on first run (~300MB)

Default port is 9377. See Environment Variables for all options.

Docker

docker build -t camofox-browser .
docker run -p 9377:9377 camofox-browser

Fly.io / Railway

fly.toml and railway.toml are included. Deploy with fly deploy or connect the repo to Railway.

Usage

Cookie Import

Import cookies from your browser into Camoufox to skip interactive login on sites like LinkedIn, Amazon, etc.

Setup

1. Generate a secret key:

# macOS / Linux
openssl rand -hex 32

2. Set the environment variable before starting OpenClaw:

export CAMOFOX_API_KEY="your-generated-key"
openclaw start

The same key is used by both the plugin (to authenticate requests) and the server (to verify them). Both run from the same environment — set it once.

Why an env var? The key is a secret. Plugin config in openclaw.json is stored in plaintext, so secrets don't belong there. Set CAMOFOX_API_KEY in your shell profile, systemd unit, Docker env, or Fly.io secrets.

Cookie import is disabled by default. If CAMOFOX_API_KEY is not set, the server rejects all cookie requests with 403.

3. Export cookies from your browser:

Install a browser extension that exports Netscape-format cookie files (e.g., "cookies.txt" for Chrome/Firefox). Export the cookies for the site you want to authenticate.

4. Place the cookie file:

mkdir -p ~/.camofox/cookies
cp ~/Downloads/linkedin_cookies.txt ~/.camofox/cookies/linkedin.txt

The default directory is ~/.camofox/cookies/. Override with CAMOFOX_COOKIES_DIR.

5. Ask your agent to import them:

Import my LinkedIn cookies from linkedin.txt

The agent calls camofox_import_cookies → reads the file → POSTs to the server with the Bearer token → cookies are injected into the browser session. Subsequent camofox_create_tab calls to linkedin.com will be authenticated.

How it works

~/.camofox/cookies/linkedin.txt          (Netscape format, on disk)
        │
        ▼
camofox_import_cookies tool              (parses file, filters by domain)
        │
        ▼  POST /sessions/:userId/cookies
        │  Authorization: Bearer <CAMOFOX_API_KEY>
        │  Body: { cookies: [Playwright cookie objects] }
        ▼
camofox server                           (validates, sanitizes, injects)
        │
        ▼  context.addCookies(...)
        │
Camoufox browser session                 (authenticated browsing)
  • cookiesPath is resolved relative to the cookies directory — path traversal outside it is blocked
  • Max 500 cookies per request, 5MB file size limit
  • Cookie objects are sanitized to an allowlist of Playwright fields

Standalone server usage

curl -X POST http://localhost:9377/sessions/agent1/cookies \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_CAMOFOX_API_KEY' \
  -d '{"cookies":[{"name":"foo","value":"bar","domain":"example.com","path":"/","expires":-1,"httpOnly":false,"secure":false}]}'

Docker / Fly.io

docker run -p 9377:9377 \
  -e CAMOFOX_API_KEY="your-generated-key" \
  -v ~/.camofox/cookies:/home/node/.camofox/cookies:ro \
  camofox-browser

For Fly.io:

fly secrets set CAMOFOX_API_KEY="your-generated-key"

Proxy + GeoIP

Route all browser traffic through a proxy with automatic locale, timezone, and geolocation derived from the proxy's IP address via Camoufox's built-in GeoIP.

Set these environment variables before starting the server:

export PROXY_HOST=166.88.179.132
export PROXY_PORT=46040
export PROXY_USERNAME=myuser
export PROXY_PASSWORD=mypass
npm start

Or in Docker:

docker run -p 9377:9377 \
  -e PROXY_HOST=166.88.179.132 \
  -e PROXY_PORT=46040 \
  -e PROXY_USERNAME=myuser \
  -e PROXY_PASSWORD=mypass \
  camofox-browser

When a proxy is configured:

  • All traffic routes through the proxy
  • Camoufox's GeoIP automatically sets locale, timezone, and geolocation to match the proxy's exit IP
  • Browser fingerprint (language, timezone, coordinates) is consistent with the proxy location
  • Without a proxy, defaults to en-US, America/Los_Angeles, San Francisco coordinates

Structured Logging

All log output is JSON (one object per line) for easy parsing by log aggregators:

{"ts":"2026-02-11T23:45:01.234Z","level":"info","msg":"req","reqId":"a1b2c3d4","method":"POST","path":"/tabs","userId":"agent1"}
{"ts":"2026-02-11T23:45:01.567Z","level":"info","msg":"res","reqId":"a1b2c3d4","status":200,"ms":333}

Health check requests (/health) are excluded from request logging to reduce noise.

Basic Browsing

# Create a tab
curl -X POST http://localhost:9377/tabs \
  -H 'Content-Type: application/json' \
  -d '{"userId": "agent1", "sessionKey": "task1", "url": "https://example.com"}'

# Get accessibility snapshot with element refs
curl "http://localhost:9377/tabs/TAB_ID/snapshot?userId=agent1"
# → { "snapshot": "[button e1] Submit  [link e2] Learn more", ... }

# Click by ref
curl -X POST http://localhost:9377/tabs/TAB_ID/click \
  -H 'Content-Type: application/json' \
  -d '{"userId": "agent1", "ref": "e1"}'

# Type into an element
curl -X POST http://localhost:9377/tabs/TAB_ID/type \
  -H 'Content-Type: application/json' \
  -d '{"userId": "agent1", "ref": "e2", "text": "hello", "pressEnter": true}'

# Navigate with a search macro
curl -X POST http://localhost:9377/tabs/TAB_ID/navigate \
  -H 'Content-Type: application/json' \
  -d '{"userId": "agent1", "macro": "@google_search", "query": "best coffee beans"}'

API

Tab Lifecycle

| Method | Endpoint | Description | |--------|----------|-------------| | POST | /tabs | Create tab with initial URL | | GET | /tabs?userId=X | List open tabs | | GET | /tabs/:id/stats | Tab stats (tool calls, visited URLs) | | DELETE | /tabs/:id | Close tab | | DELETE | /tabs/group/:groupId | Close all tabs in a group | | DELETE | /sessions/:userId | Close all tabs for a user |

Page Interaction

| Method | Endpoint | Description | |--------|----------|-------------| | GET | /tabs/:id/snapshot | Accessibility snapshot with element refs. Query params: includeScreenshot=true (add base64 PNG), offset=N (paginate large snapshots) | | POST | /tabs/:id/click | Click element by ref or CSS selector | | POST | /tabs/:id/type | Type text into element | | POST | /tabs/:id/press | Press a keyboard key | | POST | /tabs/:id/scroll | Scroll page (up/down/left/right) | | POST | /tabs/:id/navigate | Navigate to URL or search macro | | POST | /tabs/:id/wait | Wait for selector or timeout | | GET | /tabs/:id/links | Extract all links on page | | GET | /tabs/:id/screenshot | Take screenshot | | POST | /tabs/:id/back | Go back | | POST | /tabs/:id/forward | Go forward | | POST | /tabs/:id/refresh | Refresh page |

YouTube Transcript

| Method | Endpoint | Description | |--------|----------|-------------| | POST | /youtube/transcript | Extract captions from a YouTube video |

curl -X POST http://localhost:9377/youtube/transcript \
  -H 'Content-Type: application/json' \
  -d '{"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "languages": ["en"]}'
# → { "status": "ok", "transcript": "[00:18] ♪ We're no strangers to love ♪\n...", "video_title": "...", "total_words": 548 }

Uses yt-dlp when available (fast, no browser needed). Falls back to a browser-based intercept method if yt-dlp is not installed — this is slower and less reliable due to YouTube ad pre-rolls.

Server

| Method | Endpoint | Description | |--------|----------|-------------| | GET | /health | Health check | | POST | /start | Start browser engine | | POST | /stop | Stop browser engine |

Sessions

| Method | Endpoint | Description | |--------|----------|-------------| | POST | /sessions/:userId/cookies | Add cookies to a user session (Playwright cookie objects) |

Search Macros

@google_search · @youtube_search · @amazon_search · @reddit_search · @reddit_subreddit · @wikipedia_search · @twitter_search · @yelp_search · @spotify_search · @netflix_search · @linkedin_search · @instagram_search · @tiktok_search · @twitch_search

Reddit macros return JSON directly (no HTML parsing needed):

  • @reddit_search - search all of Reddit, returns JSON with 25 results
  • @reddit_subreddit - browse a subreddit (e.g., query "programming"/r/programming.json)

Environment Variables

| Variable | Description | Default | |----------|-------------|---------| | CAMOFOX_PORT | Server port | 9377 | | CAMOFOX_API_KEY | Enable cookie import endpoint (disabled if unset) | - | | CAMOFOX_ADMIN_KEY | Required for POST /stop | - | | CAMOFOX_COOKIES_DIR | Directory for cookie files | ~/.camofox/cookies | | MAX_SESSIONS | Max concurrent browser sessions | 50 | | MAX_TABS_PER_SESSION | Max tabs per session | 10 | | SESSION_TIMEOUT_MS | Session inactivity timeout | 1800000 (30min) | | BROWSER_IDLE_TIMEOUT_MS | Kill browser when idle (0 = never) | 300000 (5min) | | HANDLER_TIMEOUT_MS | Max time for any handler | 30000 (30s) | | MAX_CONCURRENT_PER_USER | Concurrent request cap per user | 3 | | MAX_OLD_SPACE_SIZE | Node.js V8 heap limit (MB) | 128 | | PROXY_HOST | Proxy hostname or IP | - | | PROXY_PORT | Proxy port | - | | PROXY_USERNAME | Proxy auth username | - | | PROXY_PASSWORD | Proxy auth password | - |

Architecture

Browser Instance (Camoufox)
└── User Session (BrowserContext) - isolated cookies/storage
    ├── Tab Group (sessionKey: "conv1")
    │   ├── Tab (google.com)
    │   └── Tab (github.com)
    └── Tab Group (sessionKey: "conv2")
        └── Tab (amazon.com)

Sessions auto-expire after 30 minutes of inactivity. The browser itself shuts down after 5 minutes with no active sessions, and relaunches on the next request.

Testing

npm test              # all tests
npm run test:e2e      # e2e tests only
npm run test:live     # live site tests (Google, macros)
npm run test:debug    # with server output

npm

npm install @askjo/camofox-browser

Credits

Crypto Scam Warning

Sketchy people are doing sketchy things with crypto tokens named "Camofox" now that this project is getting attention. Camofox is not a crypto project and will never be one. Any token, coin, or NFT using the Camofox name has nothing to do with us.

License

MIT