Skip to main content

🔌 graftpunk

Authenticated browser sessions, captured once and replayed over plain HTTP: stealth login, encrypted at rest, pluggable storage.

PyPI Python 3.11+ License: MIT Code style: ruff Typed

InstallationQuick StartPluginsCLI ReferenceExamplesArchitecture

Log in through a real browser once; graftpunk captures the authenticated session — cookies, browser-fingerprinted headers, CSRF/API tokens — encrypts it at rest, and replays it over plain HTTP from Python or a generated CLI, so a site's own XHR/JSON endpoints become scriptable without a WebDriver in the loop.


The Problem

Plenty of services you have an account with expose no API — an ISP portal, a school or medical portal, a niche shop, a municipal records site. The data is yours and it is one login away, but every request has to look like it came from a browser that already signed in: the right cookies, the browser's own headers, whatever CSRF or bearer token the page minted. Reproducing that by hand for every script is the actual chore.

The Solution

graftpunk does the login in a real browser (yours, or a declaratively scripted one), captures the resulting session and header fingerprint, stores it encrypted, and hands it back as a requests-compatible session — locally, or from S3/Supabase when the same session needs to be shared.

  1. LOG IN              2. CACHE               3. SCRIPT

  +-------------+       +-------------+       +-------------+
  |   Browser   |       |  Encrypted  |       |   Python    |
  |   Session   |------>|   Storage   |------>|   Script    |
  |             |       |             |       |             |
  +-------------+       +-------------+       +-------------+

  Log in manually       Session cached        Use the session
  or declaratively      with AES-128          with real browser
  via plugin config     encryption            headers replayed

Once your session is cached, you can:

  • Make HTTP requests with your authenticated cookies and real browser headers
  • Reverse-engineer XHR calls from browser dev tools
  • Build CLI tools that feel like real APIs
  • Automate downloads of documents and data
  • Keep sessions alive with background daemons
  • Capture network traffic for debugging and auditing

What You Can Build

Each of these is a plugin command backed by the cached session; graftpunk generates the CLI, injects the session and tokens, and formats the output:

# Pull your kid's grades and assignments
gp schoolportal grades --student emma --format table

# Download your medical lab results
gp mychart labs --after 2024-06-01 --output ./results/

# Export your energy usage data
gp utility usage --months 12 --format csv > energy.csv

# Scrape your property tax history
gp county assessor --parcel 12345 --format json

# Make ad-hoc requests with cached session cookies + browser headers
gp http get -s mychart https://mychart.example.com/api/appointments

These aren't real APIs—they're commands defined in graftpunk plugins that replay the same XHR calls the website makes. To the server, it looks like a browser. To you, it's just automation.

Installation

Logging in drives a real browser, so the usual install includes the browser stack:

pip install 'graftpunk[browser]'

Lite install — no browser:

pip install graftpunk

This gives you the full gp CLI and API replay against an already-cached session (load_session_for_api, load_session_for_api_from_bytes). It cannot log in, because that needs a browser. Base dependencies are pure-Python or ship WASM wheels, so this is also the install that works under Pyodide and Cloudflare Python Workers (#121). Add [browser] whenever you need gp <site> login or BrowserSession.

With cloud storage:

pip install graftpunk[supabase]   # Supabase backend
pip install graftpunk[s3]         # AWS S3 backend
pip install graftpunk[all]        # Everything (includes browser)

Quick Start

1. Cache a Session

The fastest way is with a plugin. Here's the httpbin example (no auth needed):

# Drop a YAML plugin into your plugins directory
mkdir -p ~/.config/graftpunk/plugins
cp examples/plugins/httpbin.yaml ~/.config/graftpunk/plugins/

# Use it immediately
gp httpbin ip
gp httpbin headers
gp httpbin status --code 418  # I'm a teapot!

For sites that require authentication, plugins can define declarative login:

# Log in via auto-generated command (opens browser, fills form, caches session)
gp quotes login

# Use the cached session for API calls
gp quotes list
gp quotes random

2. Use It Programmatically

from graftpunk import GraftpunkClient

# Use plugin commands from Python — same session, tokens, and retries as the CLI
with GraftpunkClient("mybank") as client:
    accounts = client.accounts()
    statements = client.statements(month="january", year=2024)

    # Grouped commands use nested attribute access
    detail = client.accounts.detail(id=42)

For lower-level access without plugins, load a session directly:

from graftpunk import load_session_for_api

# Returns a GraftpunkSession with browser headers pre-loaded
api = load_session_for_api("mysite")
response = api.get("https://app.example.com/api/internal/documents")

If you already hold the encrypted session blob and can't reach graftpunk's storage or key file — a Cloudflare Python Worker that read it through an R2 binding, say — go straight from bytes:

from graftpunk import load_session_for_api_from_bytes

api = load_session_for_api_from_bytes(encrypted_bytes, key=fernet_key)
response = api.get("https://app.example.com/api/internal/documents")

Both work on the lite install (no [browser]). key= is optional and defaults to graftpunk's configured key sources; pass it when the key lives somewhere graftpunk can't see, like a Worker secret. A bad key raises EncryptionError (fix the key); an unusable session raises SessionExpiredError (log in again).

3. Keep It Alive

Sessions expire. graftpunk can keep them alive in the background with the keepalive daemon.

Features

Feature Why It Matters
🥷 Stealth Mode Multiple backends: Selenium with undetected-chromedriver, or NoDriver for CDP-direct automation without WebDriver detection. Bot-detection cookies (Akamai, etc.) are automatically filtered during cookie injection to prevent WAF rejection.
🔒 Encrypted Storage Sessions encrypted with AES-128 (Fernet). Local by default, optional cloud storage.
🔑 Declarative Login Define login flows with CSS selectors. graftpunk opens the browser, fills the form, and caches the session. Works in both Python and YAML plugins.
🌐 Browser Header Replay Captures real browser headers during login and replays them in API calls. Requests look like they came from Chrome, not Python.
🔌 Plugin System Full command framework with CommandContext, resource limits, output formatting, and auto-generated CLI. Python for complex logic, YAML for simple calls.
🛡️ Token & CSRF Support Declarative token extraction from cookies, headers, or page content. EAFP injection with automatic 403 retry. Tokens cached through session serialization.
📡 Observability Capture screenshots, HAR files, console logs, and network traffic. Interactive mode lets you browse manually while recording.
🔄 Keepalive Daemon Background daemon pings sites periodically to prevent session timeout.
🛠️ Ad-hoc HTTP gp http get -s <session> <url> — make one-off authenticated requests without writing a plugin.
📊 Multi-View Output Commands can define multiple views on response data. Table format renders each view as a separate section. XLSX creates one worksheet per view. --view lets you cherry-pick views and columns.
🎨 Beautiful CLI Rich terminal output with spinners, tables, and color. --format json|table|csv|xlsx|raw on all commands.

Plugins

graftpunk is extensible via Python classes or YAML configuration. Both support declarative login, resource limits, and output formatting.

YAML Plugin (Simple REST Calls)

For straightforward HTTP calls, no Python needed:

# ~/.config/graftpunk/plugins/mybank.yaml
site_name: mybank
base_url: "https://secure.mybank.com"

login:
  url: /login
  fields:
    username: "input#email"
    password: "input#password"
  submit: "button[type=submit]"

commands:
  accounts:
    help: "List all accounts"
    method: GET
    url: "/api/accounts"
    jmespath: "accounts[].{id: id, name: name, balance: balance}"

  statements:
    help: "Get statements for a month"
    method: GET
    url: "/api/statements"
    params:
      - name: month
        required: true
        help: "Month name"
      - name: year
        type: int
        default: 2024
    timeout: 30
    max_retries: 2

Python Plugin (Complex Logic)

from graftpunk.plugins import CommandContext, LoginConfig, SitePlugin, command


class MyBankPlugin(SitePlugin):
    site_name = "mybank"
    base_url = "https://secure.mybank.com"
    backend = "nodriver"  # or "selenium"
    api_version = 1

    login_config = LoginConfig(
        url="/login",
        fields={"username": "input#email", "password": "input#password"},
        submit="button[type=submit]",
        success=".dashboard",
    )

    @command(help="List all accounts")
    def accounts(self, ctx: CommandContext):
        return ctx.session.get(f"{self.base_url}/api/accounts").json()

    @command(help="Get statements for a month")
    def statements(self, ctx: CommandContext, month: str, year: int = 2024):
        url = f"{self.base_url}/api/statements/{year}/{month}"
        return ctx.session.get(url).json()

Using Plugins

# Login (auto-generated from declarative config)
gp mybank login

# Run commands
gp mybank accounts
gp mybank statements --month january --year 2024 --format table

# List all discovered plugins
gp plugins

Plugin Discovery

Plugins are discovered from three sources:

  1. Entry points — Python packages registered via pyproject.toml
  2. YAML files~/.config/graftpunk/plugins/*.yaml and *.yml
  3. Python files~/.config/graftpunk/plugins/*.py

If two plugins share the same site_name, registration fails with an error showing both sources. No silent shadowing.

See examples/ for working plugins and templates.

CLI Reference

$ gp --help

 Usage: gp [OPTIONS] COMMAND [ARGS]...

 🔌 graftpunk — Authenticated browser sessions, captured once and replayed over
 plain HTTP: stealth login, encrypted at rest, pluggable storage.

 Log in through a real browser once; graftpunk captures the authenticated
 session — cookies, browser-fingerprinted headers, CSRF/API tokens — encrypts
 it at rest, and replays it over plain HTTP from Python or a generated CLI, so
 a site's own XHR/JSON endpoints become scriptable without a WebDriver in the
 loop.

Commands:
  version     Show graftpunk version and installation info.
  plugins     List discovered plugins (storage, handlers, sites, CLI).
  import-har  Import HAR file and generate a graftpunk plugin.
  observe     View and manage observability data (HAR, screenshots, logs).
  session     Manage encrypted browser sessions.
  keepalive   Manage the session keepalive daemon.
  http        Make ad-hoc HTTP requests with cached session cookies.
  config      Show configuration; manage the workstation env file.

(Options and the Quick-start block are elided; the full text is gp --help.)

Session Management

gp session list              # List all cached sessions
gp session show <name>       # Session metadata (domain, cookies, expiry)
gp session clear <name>      # Remove a session (or --all)
gp session export <name>     # Export cookies to HTTPie session format
gp session use <name>        # Set active session for subsequent commands
gp session unset             # Clear active session

Ad-hoc HTTP Requests

Make authenticated requests using cached sessions without writing a plugin:

gp http get -s mybank https://secure.mybank.com/api/accounts
gp http post -s mybank https://secure.mybank.com/api/transfer --data '{"amount": 100}'

Use --role to set browser header roles (built-in or plugin-defined):

gp http get -s mybank --role xhr https://secure.mybank.com/api/status
gp http get -s mybank --role api https://secure.mybank.com/v2/data  # custom plugin role

Supports all HTTP methods: get, post, put, patch, delete, head, options.

Observability

Capture browser activity for debugging:

# Open authenticated browser and capture network traffic
gp observe -s mybank go https://secure.mybank.com/dashboard

# Interactive mode — browse manually, Ctrl+C to save
gp observe -s mybank interactive https://secure.mybank.com/dashboard

# Or use the --interactive flag on observe go
gp observe -s mybank go --interactive https://secure.mybank.com/dashboard

# View captured data
gp observe list
gp observe show mybank
gp observe clean mybank

Interactive mode opens an authenticated browser and records all network traffic (including response bodies) while you click around. Press Ctrl+C to stop — HAR files, screenshots, page source, and console logs are saved automatically.

Pass --observe full to any command to capture screenshots, HAR files, and console logs.

HAR Import

Generate plugins from browser network captures:

gp import-har auth-flow.har --name mybank

Configuration

Variable Default Description
GRAFTPUNK_STORAGE_BACKEND local Storage: local, supabase, or s3
GRAFTPUNK_CONFIG_DIR ~/.config/graftpunk Config and encryption key location
GRAFTPUNK_SESSION_TTL_HOURS 720 Session lifetime (30 days)
GRAFTPUNK_LOG_LEVEL WARNING Logging verbosity
GRAFTPUNK_LOG_FORMAT console Log format: console or json
GRAFTPUNK_BROWSER_EXECUTABLE_PATH (system Chrome) Path to a Chrome/Chromium binary for the nodriver backend (e.g. Chrome-for-Testing on machines/CI without a system Chrome install)

CLI flags: -v (info), -vv (debug), --log-format json, --observe full, --network-debug (wire-level HTTP tracing).

Workstation configuration (gp config)

Persist per-machine environment for gp in ~/.config/graftpunk/env — credentials as lazy 1Password (or any) command values, settings as statics:

gp config set GRAFTPUNK_BROWSER_EXECUTABLE_PATH "/path/to/chrome"
gp config set MYSHOP_USERNAME '$(op read "op://vault/item/username")'
gp config set MYSHOP_PASSWORD '$(op read "op://vault/item/password")'
gp config list

Static values load at startup; $(…) values run only when a command actually needs them (login, first access of an allowlisted setting, or a YAML plugin's ${VAR} header expansion) — gp --help never triggers your secret manager. Real environment variables always win over the file — a variable set to the empty string counts as unset, so an accidental export FOO= doesn't shadow a workstation-file value. Single-quote command values so your shell doesn't evaluate them at set time. See docs/rfcs/2026-07-28-workstation-env.md for the full design.

Browser Backends

graftpunk supports two browser automation backends (both included by default):

Backend Best For
selenium Simple sites, backward compatibility
nodriver Enterprise sites, better anti-detection

Why NoDriver? NoDriver uses Chrome DevTools Protocol (CDP) directly without the WebDriver binary, eliminating a common detection vector used by anti-bot systems.

Bot-detection cookie filtering: When injecting session cookies into a nodriver browser (for observe mode, token extraction, etc.), graftpunk automatically skips known WAF tracking cookies (Akamai bm_*, ak_bmsc, _abck). These cookies carry stale bot-classification state that causes WAFs to reject the browser with ERR_HTTP2_PROTOCOL_ERROR. Disable with skip_bot_cookies=False if needed.

from graftpunk import BrowserSession

# Use BrowserSession with explicit backend
session = BrowserSession(backend="nodriver", headless=False)

Custom Chrome binary: the nodriver backend auto-detects a system Chrome. Set GRAFTPUNK_BROWSER_EXECUTABLE_PATH to point it at a specific Chrome/Chromium binary (e.g. Chrome-for-Testing) on machines or CI without a system Chrome install.

Security

Your Data, Your Rules

graftpunk is for automating access to your own accounts. You're not scraping other people's data—you're building tools to access information that already belongs to you.

Some services may consider automation a ToS violation. Use your judgment.

Encryption

  • Algorithm: Fernet (AES-128-CBC + HMAC-SHA256)
  • Key storage: ~/.config/graftpunk/.session_key with 0600 permissions
  • Integrity: SHA-256 checksum validated before deserializing

Best Practices

  • Keep your encryption key secure
  • Don't share session files
  • Run graftpunk on trusted machines
  • Use unique, strong passwords for automated accounts

Pickle warning: graftpunk uses Python's pickle for serialization. Only load sessions you created.

Development

git clone https://github.com/stavxyz/graftpunk.git
cd graftpunk
just setup    # Install deps with uv
just check    # Run lint, typecheck, tests
just build    # Build for PyPI

Requires uv for development. See CONTRIBUTING.md for full guidelines.

Releasing

Releases publish to PyPI through GitHub Actions using PyPI Trusted Publishing (OIDC) — no API token is stored anywhere.

just bump X.Y.Z   # opens a version-bump PR (pyproject, uv.lock, CHANGELOG)
# merge the PR, then on an up-to-date main:
just release      # validates, tags vX.Y.Z, and pushes the tag

Pushing the tag triggers .github/workflows/release.yml, which runs the tests, builds the sdist/wheel, publishes to PyPI via OIDC, and creates the GitHub release. Because build + publish run in CI on a pinned Python, releasing no longer depends on your local interpreter or any local credentials.

To (re)publish a tag that was cut before this workflow existed, or to retry a failed publish, run the workflow manually (Actions → Release → Run workflow) with the tag (e.g. v1.9.0).

One-time setup (repo owner, on pypi.org → graftpunk → Publishing): add a GitHub Actions trusted publisher with owner stavxyz, repository graftpunk, workflow release.yml, and environment pypi.

License

MIT License—see LICENSE.

Acknowledgments


Built for automating your own data access.

Download files

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

Source Distribution

graftpunk-1.14.0.tar.gz (213.8 kB view details)

Uploaded Source

Built Distribution

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

graftpunk-1.14.0-py3-none-any.whl (236.1 kB view details)

Uploaded Python 3

File details

Details for the file graftpunk-1.14.0.tar.gz.

File metadata

  • Download URL: graftpunk-1.14.0.tar.gz
  • Upload date:
  • Size: 213.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for graftpunk-1.14.0.tar.gz
Algorithm Hash digest
SHA256 f1c8719667d5e9e9b59bf1e144c561fb26a5f4c6f6dcfa4afd2474ed64a11dd2
MD5 0afd830b4262774cccec5e65d8f88667
BLAKE2b-256 4a7bed3449650fd884d6a7894b8a464035248a9c2ae1ca5002f945a5c49d5406

See more details on using hashes here.

Provenance

The following attestation bundles were made for graftpunk-1.14.0.tar.gz:

Publisher: release.yml on stavxyz/graftpunk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file graftpunk-1.14.0-py3-none-any.whl.

File metadata

  • Download URL: graftpunk-1.14.0-py3-none-any.whl
  • Upload date:
  • Size: 236.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for graftpunk-1.14.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4e45317b0fee9d5236413ab554dd4f0da4eb06f04ce0fc2e149283e98e8f844d
MD5 4b88cb9abf995c8ed2900596dccae2e8
BLAKE2b-256 87cec6a95e6e6b8dcea353715c3b9f83ef4542ab132177409d60efeec51f2bba

See more details on using hashes here.

Provenance

The following attestation bundles were made for graftpunk-1.14.0-py3-none-any.whl:

Publisher: release.yml on stavxyz/graftpunk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.14.0 This release

2 files

1.13.1

2 files

1.13.0

2 files

1.12.0

2 files

1.11.0

2 files

1.10.0

2 files

1.9.1

2 files

1.9.0

2 files

1.8.2

2 files

1.8.1

2 files

1.8.0

2 files

1.7.1

2 files

1.7.0

2 files

1.6.1

2 files

1.6.0

2 files

1.5.0

2 files

1.4.0

2 files

1.3.0

2 files

1.2.1

2 files

1.2.0

2 files

1.1.0

2 files

1.0.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page