Skip to main content

HX

An agent harness for the terminal. Python core, its own terminal renderer, OpenRouter for models.

HX runs in your project directory, reads and edits your code, runs commands in a sandboxed shell, and shows you what every turn costs.


Install

curl -fsSL https://raw.githubusercontent.com/aletisunil/hx/main/install.sh | sh

The script bootstraps uv if you don't have it, then installs HX as an isolated tool with a pinned Python. Re-running it upgrades in place.

If you'd rather not pipe a script into a shell:

uv tool install hx-cli      # or: pipx install hx-cli

hx-cli on PyPI, hx in your terminal

The package is published as hx-cli. The command it installs is hx, and that is what you type — the longer name never appears again after install.

The plain name hx on PyPI is registered by someone else and has no releases, so uv tool install hx fails with "no versions of hx". Install hx-cli.

Name
PyPI package hx-cli
Command hx
Python import hx
Config directory ~/.hx

If the command isn't found after installing, add uv's bin directory to your PATH:

export PATH="$(uv tool dir --bin):$PATH"

Requires Python 3.11+. macOS and Linux. Update with hx upgrade.

Credentials

HX reaches models over four routes. Which one serves a turn is decided by the model id alone, so "what paid for that" is always answerable by reading it:

Model id Route Billing
anthropic/claude-sonnet-4.5, openai/gpt-5, … OpenRouter per token, API key
openai-codex/gpt-5.6-terra ChatGPT Plus/Pro your subscription
devin/swe-1-6, devin/claude-opus-5, … Devin your subscription
claude-code/claude-sonnet-5, claude-code/claude-opus-4-8, … Claude Pro/Max, via Claude Code your subscription

Credentials live in ~/.hx/auth.json, mode 0600, one entry per route - except Claude Code's, which stays with Claude Code.

OpenRouter. On first run HX asks for a key. Get one at https://openrouter.ai/keys. Change it later with /configure in the TUI, or:

hx auth          # which routes have a credential, and where from
hx auth set      # paste a new OpenRouter key (hidden input)
hx auth clear    # remove the saved key

Resolution order is HX_OPENROUTER_API_KEY, then OPENROUTER_API_KEY, then the saved file. The environment wins, and both /configure and hx auth say so — otherwise saving a key while a variable is set looks like a no-op.

ChatGPT Plus/Pro. Sign in over OAuth and run Codex models against your subscription instead of paying per token (OpenAI endorses this for OSS harnesses):

hx auth login openai-codex      # or /login inside the TUI
hx --model openai-codex/gpt-5.6-terra
hx auth logout openai-codex

The browser opens to auth.openai.com and redirects to a loopback listener on port 1455. Over SSH that redirect cannot reach your machine, so paste the final URL into the prompt instead — or set HX_LOGIN_DEVICE_CODE=1 for the device-code flow. Access tokens are refreshed automatically; /model only offers routes you are actually signed in to.

hx auth names the account's plan next to the login, because "which account is this and what is it paying for" is otherwise a JWT to decode by hand:

hx auth
# openrouter     key sk-or-…8abd     ~/.hx/auth.json
# openai-codex   signed in (plus)    ~/.hx/auth.json

Codex is documented as requiring a paid ChatGPT plan, and HX says so when the login is on a free one. It does not refuse to try: the plan on the credential has been observed deciding nothing — the backend refuses every Codex model on a plus account with the same error it gives a free one — so the switch is allowed and the backend's own answer is what reports the problem.

If every Codex model is refused, the error says so in those terms: it is the account that is being refused, not the model, and a different model id will not help. Check that the subscription is active and Codex is enabled for the account at https://chatgpt.com/codex, and that the official Codex CLI can reach it at all.

Which Codex models you get is decided by your ChatGPT account, not by HX. Signing in fetches the account's own list from the Codex backend, and /model offers exactly that; ctrl+r in /model asks again. Entitlement is per model - the same plus account can run gpt-5.6-sol and be refused gpt-5.3-codex — so a model the picker does not list is one the subscription does not cover.

How hard they think comes from the same fetch. Each Codex model publishes its own reasoning levels and its own default — GPT-6 Astra starts at low, GPT-5.6 Terra at medium — and HX asks for the model's default rather than one fixed depth. /effort changes it:

/effort            # pick from the levels this model offers
/effort high       # none | minimal | low | medium | high | xhigh | max | ultra
/effort default    # back to the model's own default

It takes effect on the next turn, is saved to ~/.hx/settings.json for later sessions, and shows on the status bar next to the model — gpt-5.6-terra (sub) · high. Levels differ per model, so a choice is clamped to what the current one offers: max runs at xhigh on GPT-5.5 rather than being refused, and the bar shows what is actually happening. Set it by hand instead with:

// ~/.hx/settings.json
{ "models": { "reasoning_effort": "high" } }

When the model list cannot be fetched (no network, an old cache), HX falls back to the models it last shipped. To add an id the backend does not advertise:

// ~/.hx/settings.json
{ "models": { "codex_models": ["gpt-5.6-terra"] } }

They join the /model picker on the next start. HX cannot ask how large an unknown model's context window is, so it assumes a conservative 200k — that costs an accurate context gauge, not the use of the model.

Devin. Sign in with the same browser flow the official devin CLI uses, and run every model your Devin plan includes - SWE-1.6, and whatever frontier models the plan offers - against the subscription:

hx auth login devin             # or /login inside the TUI
hx --model devin/swe-1-6
hx auth logout devin

Devin Enterprise signs in the same way: on the page that opens, choose "Log in with Devin for Enterprise", enter your company, and finish through its identity provider. Your admin has to grant the "Use Devin CLI" permission first. Usage counts toward the organization's allocation, and an enterprise tenant served from its own API server is followed there for both turns and the model list.

The browser opens to app.devin.ai and redirects to a loopback listener on 127.0.0.1:59653. Over SSH, paste the final redirect URL into the prompt instead. A Devin login does not refresh: when it expires, HX says so and asks you to sign in again. On a machine with no browser at all, a session token in HX_DEVIN_API_KEY or DEVIN_API_KEY is used when nothing is saved.

Signing in fetches the account's own model list, and /model offers exactly that; ctrl+r in /model asks again. Devin publishes each reasoning depth as a model of its own - Claude Opus 5 High, Claude Opus 5 Max - and HX folds each family into one model, so /effort picks the depth on Devin exactly as it does on Codex. A fast lane or a 1M-context lane stays a separate model, since it changes what a turn costs or how much fits. Router models such as devin/adaptive are resolved to a concrete model on every turn by Devin itself. If the list cannot be fetched, devin/swe-1-6 and devin/swe-1-6-fast stand in.

Claude Pro/Max. HX runs Claude models on your Claude subscription through the official Claude Code CLI, the way the Claude Agent SDK does. Install Claude Code, then:

hx auth login claude-code       # Claude Code's own sign-in; or /login inside HX
hx --model claude-code/claude-sonnet-5

HX holds no Claude token. The login is Claude Code's, kept and refreshed where Claude Code keeps it, so hx auth logout claude-code only tells you to run claude auth logout - which signs out every Claude Code on the machine. A headless machine can use a token from claude setup-token in CLAUDE_CODE_OAUTH_TOKEN. A Console login (claude auth login --console) bills per token and is refused: this route is the subscription.

How a turn works: HX starts claude -p with every one of Claude Code's own tools switched off and serves HX's tools to it as an MCP server. The model's tool calls are run, permission-checked and drawn by HX exactly as on every other route; Claude Code holds each call open until HX answers it, however long an approval takes. The system prompt is Claude Code's, with your AGENTS.md files, skills index and --append-system-prompt text added. One Claude Code process serves a conversation for as long as it can, so the prompt cache carries from turn to turn; after an interrupt, a steer while a tool runs, a compaction, /model, /effort or a resumed session, HX starts a new one on a session rebuilt from its own transcript. That costs a second or so and a partly cold cache, not the conversation.

Claude Code is started isolated from your Claude Code setup: no CLAUDE.md, no hooks, no plugins or MCP servers from ~/.claude, no auto-memory, no auto-compaction (HX compacts its own transcript), and nothing saved to Claude Code's session list. Variables that would move it off the subscription - ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL, CLAUDE_CODE_USE_BEDROCK and the rest - are removed from its environment; its TLS settings are kept (see Behind a TLS-inspecting proxy below). HX_CLAUDE_CODE_PATH points HX at a claude other than the one on PATH.

/model offers the models Claude Code lists for the account, each with the efforts it accepts for /effort; ctrl+r in /model asks again. A model Claude Code runs at its long context keeps [1m] in its id (claude-code/claude-fable-5-1[1m]), exactly as Claude Code's own /model runs it. Claude Code lists no context sizes, so a model starts at the window it was measured serving on every plan - 1M for Opus 5.5, Sonnet 5.5, Sonnet 5 and Opus 4.7, otherwise 200k - and from its first turn uses the window Claude Code reports for your account. When a usage limit is reached, HX says which one and when it resets; when a model or its 1M context needs usage credits your plan has off, HX says that instead, and what to pick without them.

Web search. Optional, and the one credential that is not a model route. It powers two tools:

Tool Does
WebSearch search the web, returning ranked results with a snippet each
WebFetch read up to 5 URLs, returned as markdown

Tavily bills per search rather than per token, so it works identically on every route - including the subscription ones, where a provider-native search tool is not on offer.

Get a key at https://app.tavily.com — the free tier is 1000 searches a month and takes no card — then save it:

hx auth set tavily      # paste the key, hidden input, saved to ~/.hx/auth.json
hx auth                 # confirm: `tavily  key tvly-a…f3d9  …/auth.json`
hx auth clear tavily    # remove it again

Or keep it in the environment instead, which wins only when nothing is saved:

export TAVILY_API_KEY=tvly-...     # HX_TAVILY_API_KEY is read first

Without a key, HX works exactly as before — WebSearch and WebFetch are simply not registered, and hx auth says so:

tavily         not configured      WebSearch and WebFetch are off

There is no nag, no failing tool and no startup warning. Leaving them out is deliberate: an advertised tool that always fails still costs cached-prefix tokens every turn, and a model told "no key" just calls it again. Adding a key turns both on from the next hx; no other setting changes.

Credits are reported per call and per session at the foot of every result — [Tavily: 1 credit, 4 this session]. A search costs 1 credit, search_depth: "advanced" costs 2, and fetching costs 1 per 5 URLs. Tavily's docs describe a usage field for this, but the live API does not send one, so HX prices every call from those published rates and marks the figure ~1 rather than passing an estimate off as measured. Check app.tavily.com for the authoritative number.

Credits are not in the status bar's cost, which prices tokens: /cost under-reports a session that searched.

Behind a TLS-inspecting proxy. HX verifies certificates against the operating system's trust store, so a corporate CA the machine already trusts needs no setup. When the CA lives only in a file, name it:

export HX_CA_BUNDLE=/path/to/corp-ca.pem   # SSL_CERT_FILE and REQUESTS_CA_BUNDLE also work

HX_SSL_NO_VERIFY=1 turns verification off entirely; it prints a warning at startup every session, because it is not a fix. A TLS failure is reported in one line - /model and the startup notice both name the certificate problem rather than reporting an empty catalogue with no cause. A file that holds no certificates is ignored, with a startup notice that says so.

"Unsupported certificate purpose" (or "unsuitable") is different: the CA is trusted, but the proxy signs with a certificate that is not valid for servers, so no CA setting fixes it. The network has to fix that certificate or exempt the service from inspection; until then only HX_SSL_NO_VERIFY=1 gets through.

The same settings cover the claude-code route, whose requests Claude Code makes with its own TLS stack: HX passes HX_CA_BUNDLE on to it as NODE_EXTRA_CA_CERTS (unless you already set that yourself), and HX_SSL_NO_VERIFY as NODE_TLS_REJECT_UNAUTHORIZED=0. Claude Code reads the OS trust store on its own, and its own TLS settings reach it untouched: CLAUDE_CODE_CERT_STORE, and CLAUDE_CODE_CLIENT_CERT, CLAUDE_CODE_CLIENT_KEY and CLAUDE_CODE_CLIENT_KEY_PASSPHRASE for a network that requires a client certificate.

HX never sends a Claude subscription token to the API itself - Anthropic accepts those only from Claude Code, and HX will not impersonate it. The claude-code route runs the real Claude Code instead.


Running it

hx                          # interactive TUI in the current directory
hx -p "explain this repo"   # headless: streams to stdout, tool activity to stderr
hx -p "why?" --image s.png  # attach an image to a headless prompt; repeatable
hx resume                   # resume the last session here
hx resume <session-id>      # resume a specific one, in the directory it was recorded in
hx prompt                   # print the system prompt this directory would use
hx trace                    # write the last session here to an HTML page
hx trace out.html           # the same, to a path you choose
hx trace <id> out.html      # a specific session, to a path you choose
hx --model openai/gpt-5     # override the model for one run
hx --mode plan              # start read-only
hx --cwd ../other-project   # run against a different directory
hx --no-sandbox             # disable OS sandboxing (rules still apply)
hx --system-prompt @p.md    # replace the system prompt for one run
hx --append-system-prompt "Always run the tests"   # add to it; repeatable
hx mcp list|add|remove      # manage MCP servers
hx mcp login <server>       # sign in to a remote MCP server in the browser
hx mcp logout <server>      # forget a remote MCP server's sign-in
hx auth [set|clear]         # manage the OpenRouter key
hx auth login [provider]    # sign in (openrouter, openai-codex, devin, claude-code)
hx auth logout <provider>   # forget a stored credential
hx docs [section]           # the manual; no argument lists its sections
hx changelog [version]      # what shipped in each version
hx upgrade                  # update to the latest release

Print mode is the scriptable one: stdout carries only the assistant's text, so it pipes cleanly.

The docs ship with the binary

README.md and CHANGELOG.md are included in the wheel, so the installed tool can print the manual and the release record for the version you are actually running:

hx docs                     # the section titles
hx docs credentials         # one section, printed whole
hx docs --all               # the entire manual
hx changelog                # every release, newest first
hx changelog 0.1.5          # one release
hx changelog unreleased     # what has landed since the last tag

The system prompt points a session at these two commands, so "how do I set the API key?" or "what changed in the last release?" is answered from the shipped documentation rather than from the model's recollection of some other version. The call still goes through the permission engine like any other command; allowlist it if you would rather not be asked:

{ "permissions": { "allow": ["Bash(hx docs:*)", "Bash(hx changelog:*)"] } }

CHANGELOG.md lists what each version added, and a release cannot skip it: tests/e2e/test_cli.py fails when __version__ has no section (see Releasing).

Keys

Every key below is defined once, in src/hx/keys.py. /help, the hints line under the prompt and this table all read from that registry, and any of them can be rebound (see Keybindings).

Key Does
enter send
shift+enter newline (also ctrl+j; in a terminal that cannot send shift+enter, end the line with \ and press enter)
esc interrupt the current turn
ctrl+c copy the selected text; with nothing selected, clear the prompt (twice on an empty prompt exits)
ctrl+d exit, when the prompt is empty
shift+tab cycle permission mode
ctrl+p command palette
ctrl+l model picker; inside it, ctrl+r fetches the catalogue again
ctrl+o expand tool output (also ctrl+r)
ctrl+t show the current plan
ctrl+x copy the selected message to the clipboard
ctrl+up previous message
ctrl+down next message
pgup scroll the transcript up (fullscreen; otherwise your terminal scrolls)
pgdn scroll the transcript down (fullscreen; otherwise your terminal scrolls)
ctrl+home jump to the start: the viewport in fullscreen, the first message otherwise
ctrl+end jump back to the newest output
ctrl+v attach the image on the clipboard (see Images)
@path complete a file path
!command run a shell command directly, no model turn

! still goes through the permission engine and the sandbox — it skips the model, not the safety layers.

The prompt is a readline-style editor: ctrl+a/ctrl+e for line start and end, ctrl+b/ctrl+f by character, alt+b/alt+f by word, ctrl+w and alt+d to kill a word, ctrl+u and ctrl+k to kill to the start or end of a line, then ctrl+y to yank it back and alt+y to walk further down the kill ring. ctrl+z undoes and ctrl+shift+z redoes (ctrl+_ undoes too). cmd+z and cmd+shift+z work in a terminal that passes cmd through to the program, such as kitty or WezTerm; Apple's Terminal, iTerm2 and Ghostty keep cmd+z for their own Edit menu. A terminal without the kitty protocol or modifyOtherKeys sends ctrl+shift+z as ctrl+z, so there it undoes. Suspending to the background has no key by default - bind app.suspend in keybindings.json (for example to "ctrl+g") to get it back.

Typing / or @ opens a completion list above the prompt; tab cycles it, enter accepts, esc dismisses.

Copying

ctrl+x copies the message the cursor is on, and /copy copies the last reply. HX writes through the platform's own clipboard tool first (pbcopy, wl-copy, xclip, xsel) and falls back to OSC 52, which is also always sent over SSH so the text lands on the machine you are actually sitting at.

Drag-select in the transcript and ctrl+c copies the selection — assistant prose and tool output included. With nothing selected, ctrl+c keeps its usual meaning and clears the prompt. See Selecting and copying text for the terminal's own selection.

HX does not emit OSC 133 prompt markers yet, so shell-integration features that jump between prompts will not see HX's messages.

Images

Give the model a picture three ways, and each lands in the prompt as a token such as [Image #1]:

  • ctrl+v attaches whatever image is on the clipboard: a screenshot copied with cmd+ctrl+shift+4, an image copied in a browser, or an image file copied in Finder or a file manager. Terminals whose own paste sends nothing when the clipboard holds a picture are caught too - an empty paste is read as a request for the image.
  • Drag an image file into the terminal. The terminal pastes its path, and a paste that is nothing but paths to image files attaches them instead of inserting the text. Quoted, backslash-escaped and file:// paths all work.
  • Mention it. look at @docs/mockup.png sends the path as text, and the model opens it with Read, which returns images as images.

The token is the attachment: move it and the text refers to the picture in a different place, delete it (one backspace takes the whole token) and the image is not sent. Sent messages list their images underneath, with size and weight. Numbers run for the whole session, so a message recalled from history or restored by a rewind still carries its pictures.

PNG, JPEG, GIF, WebP, BMP and TIFF are accepted. An image within every route's limits goes byte for byte; a larger one is scaled to 2000 px on its long side and re-encoded to stay under 5 MB, which is what a retina screenshot usually needs. Images an MCP tool returns - a browser screenshot, say - are handled the same way rather than dropped.

Whether a model can see images comes from the catalogue: OpenRouter's input modalities, the Codex catalogue, and Devin's model config. The /model picker marks the ones that can with images. A model that cannot is warned about when you attach, and is sent a line saying an image was left out, rather than a request that fails. The transcript keeps the image either way, so /model to one that can see it and ask again.

Over SSH the clipboard you copied into is on your own machine, so ctrl+v says so; copy the file over and drag or paste its path instead.

Keybindings

Any key can be rebound in ~/.hx/keybindings.json, keyed by the action ids in src/hx/keys.py:

{
  "app.tools.expand": "ctrl+r",
  "app.message.copy": ["ctrl+x", "alt+c"]
}

Modifiers are ctrl, alt (or option), shift and cmd (or super); a cmd key only arrives from a terminal that passes cmd through. Conflicts and unknown action names are reported as a notice at startup rather than being silently resolved.

Commands

Command Does
/model [query] pick a model; shows context window, price per Mtok, cache support. ctrl+r in the picker re-fetches the catalogue
/effort [level] how hard a reasoning model thinks, from the levels it offers
/configure session settings and the API key
/login [provider] sign in to a model route
/logout <provider> forget a stored credential
/mode [name] plan, default, acceptEdits, bypass
/permissions active rules and what is enforcing them
/context what is filling the context window
/cost tokens, cache savings, spend
/compact [focus] summarise older turns now
/clear fresh session, same directory
/resume switch to another session - this directory's first, then the rest
/rewind go back to an earlier prompt, restoring the files HX changed
/title [text] show or set this session's name
/prompt the system prompt this session is running with
/todos show the current plan
/skills installed skills
/agents subagent types
/mcp [login|logout|reconnect <server>] server status, and signing in to remote servers
/hooks configured hooks, and any that were refused
/theme [name] dark, light, ansi, or any theme in ~/.hx/themes
/queue [steer <n>|clear] messages waiting for the turn to end, and what to do with them
/copy copy the last reply to the clipboard
/trace [path] write the whole session to a self-contained HTML page
/fullscreen [on|off] take the whole window, with the prompt pinned to the bottom
/mouse [on|off] mouse reporting, and with it your terminal's own text selection
/init generate an AGENTS.md for the project
/help list commands and keys
/quit exit (also /exit, /q)

The two screens

By default HX draws into your terminal's own scrollback. Finished output belongs to the terminal while the session runs: it scrolls it, selects it, copies it and pipes it. Nothing is repainted above the fold, so a long session costs the same to draw as a short one.

The prompt and the status bar still open on the bottom row of the window: a conversation too short to fill the screen is padded above them rather than below, so the dock stays where you last saw it instead of creeping down the screen a block at a time. The padding is gone by the time the terminal starts scrolling.

Leaving takes the whole interface off the screen: the banner, the conversation and the dock are erased and your shell prompt comes back on the row HX started on, the terminal as it was found. The erase stops at the top of the window - lines that had already scrolled above it stay in your scrollback, because the only sequence that reaches them takes everything the terminal remembers, including what was there before HX ran. /clear is the command that asks for that.

/fullscreen switches to the alternate screen instead - the whole window, the prompt pinned to the bottom row, the transcript scrolling above it with pgup, pgdn, ctrl+home and ctrl+end. The trade is the thing the default was chosen for: the alternate screen is not scrollback, so your terminal cannot scroll or select it, and it is gone when HX exits. /fullscreen off gives it back, and either way the choice is written to tui.fullscreen for the next session. Quitting from fullscreen leaves the same clean terminal as quitting from the default screen: the alternate screen goes, and so does the frame HX had drawn underneath it before the switch.

/clear wipes the screen and the scrollback above it. Terminals that do not implement scrollback erasure - macOS Terminal.app is the one people hit - clear the screen only, and the old conversation stays above it until you clear it the terminal's own way (cmd+k).

Selecting and copying text

Drag to select and ctrl+c to copy. HX copies through pbcopy, wl-copy or xclip first and falls back to OSC 52, so it works in terminals that ignore OSC 52 - macOS Terminal ignores it outright, and iTerm2 ships with it off. The notice says which mechanism took the text, so a copy that did nothing says so.

While HX is running, the terminal reports mouse events to it, which means the terminal's own click-and-drag selection is unavailable. Two ways around that:

  • Hold alt/option while dragging. Most terminals - macOS Terminal, iTerm2, GNOME Terminal - take that as "this drag is mine", and you get the native selection without changing anything.
  • /mouse off turns mouse reporting off for the session. Native selection comes back everywhere, and HX stops seeing scroll and clicks until /mouse on.

Steering a running turn

Type while HX is working and enter queues the message: it runs when the turn ends, which is what you want for "and then do this".

alt+enter steers instead. The message goes into the turn that is running now - the model call is cut off mid-stream and the next one starts from what you just said. Tools already running are left to finish first, because cancelling a half-written file is worse than waiting a second for it.

With nothing typed, alt+enter steers the message at the front of the queue, so a correction you already queued does not have to be typed twice. /queue lists what is waiting; /queue steer 2 sends one of them now.

Set tui.enterWhileBusy to "steer" to swap the two keys around.

The status bar

Two lines: working directory and permission mode above; token counts, cache read/write with hit rate, spend, last-turn latency, context gauge and model below. The cache and cost fields are the point of it — a hit rate that collapses after an edit is the visible symptom of a broken prefix.


Configuration

Settings are JSON, merged lowest to highest:

defaults  <  ~/.hx/settings.json  <  ./.hx/settings.json  <  ~/.hx/projects/<project>/settings.local.json  <  HX_* env  <  CLI flags

Permission rule lists and prompt.append are unioned across layers, so a project can add a deny rule — or a line to the system prompt — without discarding yours. Everything else is replaced.

./.hx/settings.json is the project's shared file, yours to check in, and nothing writes to it but you — HX only ever reads it. Versions before the local layer existed did append grants there; those are lifted out into the file below on the next run, once, and a rule you put back by hand afterwards stays put. Only allow moves: a project's deny and ask are the guardrails it shares, and HX never wrote them.

Anything HX decides on your behalf - an "always allow" grant, for one - goes in ~/.hx/projects/<project>/settings.local.json instead. Still per project, because a grant is: allowing pytest in one repo should not allow it everywhere. But under your home rather than in the checkout, so HX never leaves a file in your repository for you to find in a diff. If an older version already wrote ./.hx/settings.local.json, it is moved there on the next run, along with the .hx/.gitignore it added to hide it.

{
  "theme": "dark",                    // dark | light | ansi, or a file in ~/.hx/themes
  "quiet_startup": false,             // skip the startup header

  "models": {
    "model": "anthropic/claude-sonnet-4.5",
    "subagent_model": null,           // defaults to "model"
    "title_model": null,              // model that names sessions; defaults to "model"
    "max_tokens": 8192,
    "temperature": null,
    "reasoning_effort": null,         // Codex route: null = the model's own default
    "codex_models": []                // extra Codex ids, e.g. ["gpt-5.6-terra"]
  },

  "permissions": {
    "mode": "default",                // plan | default | acceptEdits | bypass
    "allow": ["Bash(git status:*)"],
    "ask":   [],
    "deny":  ["Read(**/.env)"],
    "sandbox": true,
    "allow_network": false            // outbound network for sandboxed commands
  },

  "context": {
    "compact_at": 0.80,               // fraction of the window that triggers compaction
    "keep_recent_turns": 6,           // turns kept verbatim across a compaction
    "tool_output_char_cap": 25000,
    "tool_output_line_cap": 2000,
    "git_notices": true               // late-inject the branch and outside file changes
  },

  "bash": {
    "timeout_seconds": 120,
    "max_timeout_seconds": 600,       // ceiling; caps what the model may ask for
    "shell": null                     // defaults to $SHELL
  },

  "prompt": {
    "system": null,                   // replaces the built-in system prompt
    "append": []                      // added after it; unioned across layers
  },

  "tui": {
    "enterWhileBusy": "queue",        // queue | steer - what enter does mid-turn
    "fullscreen": false               // true takes the whole window (see /fullscreen)
  },

  "tools": {
    "hashline": true                  // Read labels lines with content anchors Edit accepts
  }
}

hooks is configured in the same files but is deliberately not part of this merge: it is read per layer, and only from your own settings. See Extending it.

Environment overrides: HX_MODEL, HX_SUBAGENT_MODEL, HX_MAX_TOKENS, HX_PERMISSION_MODE, HX_SANDBOX, HX_COMPACT_AT, HX_GIT_NOTICES, HX_THEME, HX_QUIET_STARTUP. Also HX_HOME to relocate user state, HX_TAVILY_API_KEY for web search, HX_CA_BUNDLE / HX_SSL_NO_VERIFY for TLS (see Credentials), and HX_OPENROUTER_BASE_URL to point the OpenRouter route at another OpenAI-compatible endpoint - a gateway, or a recording proxy. HX_MODIFIER_PROBE=0 stops HX asking macOS whether shift is held when an enter arrives (see Keys).

The system prompt

hx prompt prints the prompt this directory resolves to, with its source on stderr so stdout stays pipeable. /prompt shows the same thing inside a running session.

The built-in prompt is SYSTEM_PROMPT in src/hx/core/context.py. It is replaced by the first of these that exists:

Source Scope
--system-prompt TEXT or @path, or prompt.system one run
./.hx/system-prompt.md this project
~/.hx/system-prompt.md you, everywhere
the built-in prompt fallback

Appended text is added after whichever prompt won, in this order: ~/.hx/system-prompt-append.md, ./.hx/system-prompt-append.md, then each --append-system-prompt value (and prompt.append) in the order given. Nothing appended is ever discarded by a later layer.

This is a different lever from AGENTS.md: AGENTS.md describes the project and is injected as its own context section, while these files change the agent's instructions. Both are read once at startup and frozen, because they sit above the first cache breakpoint — an override edited mid-session applies on the next run, and /prompt says so when it spots one.

Sessions are named

After the first exchange, HX asks the model for a short name for the session and writes it to ~/.hx/sessions/<id>/meta.json, so /resume lists work rather than timestamps. It is one small call — cap 32 output tokens, models.title_model if you want a cheaper model for it — and it is counted in /cost like any other. If the call fails, or you leave before it is back, the session is still named, from your first message.

The live session is never renamed while it runs, and nothing is renamed on /exit, which returns your shell at once. A session you leave with /clear or /resume - including a /resume into another directory - is renamed in the background for what it became. /title <text> renames it yourself, and a name you chose is never replaced.

/resume lists every session except the one you are in: this directory's first, then other directories', each row naming where it lives. A session from this directory is swapped in place. One from elsewhere belongs to that directory - its sandbox, permission rules, prompt and tools - so HX restarts itself there with hx resume <id>, keeping options like --mode and --model.

Themes

A theme is a JSON file: a vars block of raw colours, and a colors block mapping semantic roles onto them. Drop one in ~/.hx/themes/mine.json and /theme mine picks it up — no restart, no code change.

{
  "name": "mine",
  "dark": true,
  "vars": { "green": "#b5bd68", "gray": "#808080" },
  "colors": {
    "background": "#18181e",
    "success": "green",           // a vars key, or a literal
    "muted": "gray"
    // ...every role; a missing one is an error, not a silent black
  }
}

The role names are pi's, so pi theme files load here unchanged. src/hx/tui/themes/dark.json is the reference.

Where things live

Path What
~/.hx/settings.json your settings
~/.hx/auth.json API keys and logins, mode 0600
~/.hx/models.json cached model catalogue, refreshed daily
~/.hx/themes/ your themes, one JSON file each
~/.hx/keybindings.json your key overrides
~/.hx/system-prompt.md your system prompt, replacing the built-in one
~/.hx/system-prompt-append.md text appended to whichever prompt is in force
~/.hx/AGENTS.md your instructions, loaded into every session in every project; created empty
~/.hx/sessions/ transcripts, spilled tool output, subagent sessions
~/.hx/sessions/<id>/trace.html where /trace writes, unless you name a path
~/.hx/skills/, ~/.hx/agents/ your skills and agents
~/.hx/projects/<project>/settings.local.json this machine's settings for one project - where "always allow" lands
./.hx/settings.json project settings, checked in if you like — read by HX, never written
./.hx/mcp.json project MCP servers
./.hx/system-prompt.md, ./.hx/system-prompt-append.md project prompt overrides
./.hx/skills/, ./.hx/agents/ project skills and agents
AGENTS.md project instructions: the repository root's, then each one down to where HX started

AGENTS.md is the place for things a newcomer would get wrong: how to run the tests, conventions, what not to touch. /init writes a first draft. It is loaded once per session and frozen, so it costs one prefix, not one per turn.

~/.hx/AGENTS.md is the same thing for you rather than the project: how you like commits written, tools you always want used, in every repository. It is loaded first and the project's AGENTS.md after it, so where the two disagree the project has the last word. The first session creates it empty, ready to fill in; empty, it adds nothing. hx prompt and /prompt list each one in force.

Started below the repository root - in services/billing, say - HX still loads the root's AGENTS.md, then every AGENTS.md on the way down to that directory, the nearest last, so a package can refine the repository's rules. A submodule counts as part of the repository around it. Nothing above the repository root is read; outside a repository only the current directory is. Subagents run under the same files as the session that started them.


Safety

Two independent layers guard every tool call, and both must pass.

Permission rules are Tool(specifier) strings — Bash(git commit:*), Edit(src/**), Read(**/.ssh/**). Deny beats ask beats allow, and a deny holds even in bypass mode. Shell commands are decomposed into their real segments first, so an allow rule for git status does not carry && rm -rf / along with it; a command that cannot be decomposed with confidence prompts rather than passing.

An OS sandbox wraps command execution: Seatbelt on macOS, bubblewrap on Linux. The filesystem is readable, writes are confined to the project and the temp dir, credential paths (~/.ssh, ~/.aws, and HX's own auth.json) are unreadable and unwritable - even inside the project, and even before they exist - and outbound network is off. If neither backend is present the status bar says no-sandbox rather than implying protection that is not there.

Modes cycle with shift+tab: plan (read-only — mutating tools are not even offered to the model), default, acceptEdits, bypass.


Context engineering

Long sessions are the normal case, so the harness is built around keeping the provider's KV cache warm and the window from filling up.

A stable prefix. System prompt, tool schemas and project context are assembled in a fixed order and never mutated mid-session. Cache breakpoints sit at the end of that static block and at a rolling point before the recent turns, which only advances once enough tokens have accumulated behind it.

Late injection carries everything that changes per turn - the todo list, files that changed on disk since HX read them, the git branch and any file the working tree gained or lost outside the session - on the tail of the newest user message rather than in the prefix. Stale copies are stripped and regenerated each turn, so six todo updates leave one copy in context, not six.

Working-tree notices come from one git status --porcelain=v2 --branch -z per provider call, run without a shell and bounded by a two-second timeout. It runs in a worker thread, as every injector does: a turn makes one of these per tool round-trip, and on the event loop a slow repository would freeze the UI each time. A file HX has already read is left to the stale-file notice, so no path is reported twice; outside a repository the watcher disables itself for the session. Turn it off with "context": {"git_notices": false}.

Rewind (/rewind) picks an earlier prompt and takes the session back to it: the transcript is cut there and every file HX wrote after it is restored from a content-addressed snapshot taken before each Write and Edit. A file someone else changed in the meantime is reported and left alone, and shell commands are outside the scheme - the report says so rather than implying the tree was fully returned. The prompt that was cut comes back in the input box, because a rewind is usually the first half of saying it differently. Nothing is deleted from the session file: the rewind is one more append, and replaying the file reproduces the state - which is also how an undone compaction gives its messages back. The cost ledger is deliberately not rewound; those tokens were spent.

Compaction fires at 80% of the window, or on /compact [focus]. Older turns are replaced by a structured summary; the recent turns and the todo list survive verbatim, and the boundary snaps to a turn edge so a tool call is never severed from its results. Superseded messages are flagged, not deleted, so resume replays exactly what happened.

Output capping keeps the head and tail of a large tool result, spills the rest to the session directory, and hands the model that path to grep.


Tracing a session

/trace writes everything that happened to one self-contained HTML file and opens it in a browser:

/trace                      # ~/.hx/sessions/<id>/trace.html
/trace ~/Desktop/bug.html   # somewhere you choose

Everything means everything. The system prompt, the project context, the skills index, every tool schema as the provider received it, every prompt, every reply, every thinking block, every tool call next to the result it produced, the per-call token ledger, and the files HX changed. Late-injected reminders and compacted turns are in there too, behind a toggle that says how many there are — a trace that hides the reminders and the superseded turns is not a trace of the session that ran.

Nothing is summarised and nothing is prettified. Assistant text is shown as it arrived, backticks and all, because a trace exists to answer "what actually went through" and a rendered version of that is a different document. Long tool results are folded rather than truncated, with the character count on the fold.

Thinking, where the provider allows it. How much of a model's reasoning HX ever sees is the provider's decision, not HX's. Anthropic's models return the thinking itself. The Responses API - the OpenAI and Codex routes - returns a one-line summary in plaintext and the reasoning encrypted, to be replayed on the next call and read by nobody, HX included. A trace labels which of the two it is holding, because a one-line thinking block is otherwise indistinguishable from a trace that dropped the rest.

A running session traces as it stands right now, including the turn that just landed. The page says so, because a trace of a live session is a snapshot.

Reading it. Long tool results are folded and the reference panels - the prompt, the schemas, the ledger - start shut, so the transcript is what you land on. "Expand all" opens the whole page, panels included, and turns into "Collapse all". A session with no turns yet opens its panels on its own, since there is nothing else to read.

Dark or light. The page follows your system by default and a button in the corner overrides it, remembered per browser. No stylesheet is fetched to do it.

Nothing is uploaded and nothing is fetched. The payload, the stylesheet and the script all live in the file, so it opens offline, renders the same a year later, and a trace of a private repository stays as private as the directory you wrote it into. The embedded payload is also the machine-readable trace: it is JSON, in a <script id="trace-data"> element, and /trace is the only way HX will ever put your transcript anywhere but ~/.hx.

From outside a session, on any transcript on the machine:

hx trace                        # the last session in this directory
hx trace 20260918-214743-b5b8f9 # one by id
hx trace ~/somewhere.html       # the last session, to a path you choose
hx trace <id> ~/somewhere.html  # both, to a path or a directory
open "$(hx trace)"              # the path is all that goes to stdout

hx trace reads the transcript and runs nothing, so it works on a session that ended months ago and on one another terminal has open right now.

What a trace can be missing. The prompt and the tool schemas are written to the transcript on the session's first provider call — the first moment the tool registry has settled, because MCP servers connect after the loop is built. A session traced before that call, or one recorded by a version of HX older than this feature, has the transcript but not the prompt, and the page leaves those panels out rather than guessing. /trace inside a live session fills them in from the loop either way.


Reading and editing code

Line anchors. Read labels every line with a short content hash:

     1 a3f9	def handle(request):
     2 7c1c	    if request.method == "POST":

The anchor is a hash of the line and its two neighbours, truncated to the narrowest width that is unambiguous in that file. Edit takes those anchors instead of retyped content:

{ "file_path": "app.py", "hashline": [{ "start": "a3f9", "end": "7c1c", "new_string": "..." }] }

The span is inclusive; omit end to replace one line, pass an empty new_string to delete. Anchors are recomputed from disk when the edit runs, so a file that moved under the model fails to resolve and the patch is rejected rather than applied at stale coordinates. Exact-match old_string still works and is unchanged; anchors are a second shape, not a replacement. Turn the whole thing off with "tools": {"hashline": false} and reads go back to plain cat -n.

Images. Read on a PNG, JPEG, GIF, WebP, BMP or TIFF - or on a file with no extension whose bytes are one - returns the image itself for the model to look at, normalised the way a pasted one is (see Images).

Symbols answers structural questions with a parse tree rather than a regex — three modes, outline for one file's shape, definition for where a name is defined, references for where it is used:

Symbols(mode="references", symbol="handle")

Because it matches identifier nodes, the same word in a comment or a string literal never matches. It cannot resolve types, so an unrelated symbol with the same name in another file still does; it is tighter than Grep, not a language server. Python, TypeScript, JavaScript, Go and Rust.

It needs grammars that are not installed by default, and the tool is simply not registered without them — its schema never enters the cached prefix advertising something that cannot run:

uv tool install "hx-cli[symbols]"

Extending it

Skills are directories containing SKILL.md with YAML frontmatter:

---
name: deploy
description: Tag, build and ship a release
allowed-tools: Read, Bash          # optional; narrows the toolset while active
---

1. Run the tests.
2. Tag the commit.

Drop them in .hx/skills/<name>/ or ~/.hx/skills/<name>/; a project skill shadows a user one of the same name. Only the name and description enter the context — the body loads when the model calls Skill(name), so a hundred installed skills cost a hundred lines, not a hundred documents.

Subagents run in their own context with their own transcript, tool allowlist and model. Only the final report returns to the parent, so a long search costs the caller one paragraph instead of every intermediate tool result. explore, plan and general ship built in; add your own as .hx/agents/<name>.md:

---
name: reviewer
description: Reviews a diff against the project's conventions
tools: Read, Grep
model: openai/gpt-5                 # optional
---

You review code. Be specific and cite file:line.

A subagent never gets the Task tool, so recursion is impossible by construction.

MCP servers go in .hx/mcp.json:

{
  "mcpServers": {
    "local": { "command": "python", "args": ["server.py"] },
    "remote": { "url": "https://example.com/mcp" }
  }
}

Or hx mcp add local python server.py, or hx mcp add remote --url https://example.com/mcp. Tools arrive namespaced mcp__<server>__<tool> in a deterministic order. Servers connect concurrently with a per-server timeout; one that is broken or slow logs a warning and is dropped rather than taking the session with it. A server that changes its tool list mid-session (notifications/tools/list_changed) has the new list swapped in without a restart.

A remote server that asks for sign-in - Atlassian's, for one - is signed in to over OAuth, with nothing to configure:

{ "mcpServers": { "atlassian": { "url": "https://mcp.atlassian.com/v2/mcp" } } }

It shows as needing sign-in until you run /mcp login atlassian in a session or hx mcp login atlassian in a shell. That opens the browser, and the tools arrive as soon as you approve - no restart. HX follows the MCP authorization spec: it finds the authorization server from the server's metadata, registers itself as a client, signs in with PKCE, and keeps the token in ~/.hx/auth.json (mode 0600), refreshing it before it expires and again if the server rejects it. Over SSH, paste the final redirect URL into the prompt instead. /mcp logout atlassian forgets it.

A server that does not offer client registration needs a client you register with it yourself, and anything else about the sign-in can be pinned the same way:

"oauth": { "clientId": "...", "clientSecret": "...", "scope": "read write", "callbackPort": 33418 }

To send a token of your own instead - an API key - give the server an Authorization header, and OAuth stays out of the way: hx mcp add jira --url https://mcp.atlassian.com/v2/mcp --header "Authorization: Basic <base64 email:token>".

Hooks are shell commands HX runs at named points in a turn. Four events: PreToolUse, PostToolUse, UserPromptSubmit and Stop.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "~/.hx/guard-bash.sh", "timeout": 10 }]
      }
    ],
    "PostToolUse": [
      { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "ruff check --quiet" }] }
    ]
  }
}

matcher is a regex matched against the whole tool name; omit it, or use *, to match every tool. The hook reads a JSON event on stdin — session_id, cwd, hook_event_name, and tool_name/tool_input where they apply — and answers with its exit code:

Exit Means
0 allow
2 block. stderr becomes the reason the model is given
anything else the hook is broken. Reported as a notice; the action proceeds

That last row is deliberate: a typo in a shell command must not be able to wedge a session. Only an explicit refusal stops anything.

A hook may also print JSON on stdout: {"decision": "block", "reason": "..."} refuses, {"updatedInput": {...}} rewrites a tool's arguments before the permission engine sees them, and {"additionalContext": "..."} adds a note for the model to read. The format matches Claude Code's, so hooks already written against that work here unchanged.

A worked example ships in the repository: examples/hooks/check.sh runs the checker a project already has — ruff, tsc, go vet, cargo check — against the one file the model just edited, and returns whatever it says as additionalContext. The model sees the errors appended to its own edit result and fixes them on the next turn, rather than discovering them at test time. It exits 0 either way: by PostToolUse the write has already landed, so refusing it would be an answer to something that already happened.

curl -o ~/.hx/check.sh https://raw.githubusercontent.com/aletisunil/hx/main/examples/hooks/check.sh
chmod +x ~/.hx/check.sh

Where hooks may be declared is a security boundary. They are loaded from ~/.hx/settings.json and ~/.hx/projects/<slug>/settings.local.json only. Hooks in a project's checked-in .hx/settings.json are ignored and listed by /hooks, because that file arrives with a clone and cloning a repository must never be enough to run commands on the machine that cloned it. To adopt a shared hook, copy it into your own settings — a deliberate act by the person who will run it. Subagents inherit the parent's hooks, so a guard cannot be bypassed by delegating through Task.


Development

git clone https://github.com/aletisunil/hx && cd hx
uv sync --extra dev

uv run pytest                # the suite; live tests are deselected
uv run ruff check . && uv run ruff format --check .
uv run mypy                  # strict
uv run hx                    # run from the checkout

The suite is end-to-end. Each test in tests/e2e/ starts the installed hx binary - on a real pseudo-terminal for the TUI, or as a plain subprocess for print mode and the other commands - in a private $HOME and $HX_HOME, and points it at a local OpenRouter stand-in with HX_OPENROUTER_BASE_URL. Everything between the keyboard and the socket is the shipped code; only the model's replies are scripted. The screen is read back through a VT emulator, so assertions are on the cells a terminal shows, not on the bytes HX meant to write.

Every run writes e2e-report/: index.html shows each test's outcome and every screen it checked, rendered in colour, and one text file per screen lets two runs be compared with diff -r. CI uploads it from every test job as an e2e-report-* artifact. Set HX_E2E_REPORT to write it somewhere else.

Tests that need the OS sandbox are skipped where there is no backend (sandbox-exec on macOS, bwrap on Linux).

Tests marked live hit a real API. The OpenRouter ones cost money:

OPENROUTER_API_KEY=... uv run pytest -m live tests/test_live.py

# the Codex route draws on your ChatGPT subscription instead
hx auth login openai-codex
uv run pytest -m live tests/test_live_codex.py

# and the Devin route on your Devin subscription
hx auth login devin
uv run pytest -m live tests/test_live_devin.py

They are the only place the wire formats, streaming, tool use and a genuine cache hit are proven against a real service; everything else runs against the stand-in.

Layout

src/hx/
  cli.py config.py paths.py frontmatter.py git.py
  core/         loop, context assembly, compaction, late injection, sessions, usage
  auth/         credential store, OAuth flows, per-route resolution
  providers/    OpenRouter, Codex, Devin, the model catalogue
  tools/        Bash, Read, Write, Edit, Glob, Grep, Symbols, TodoWrite, Task,
                WebSearch, WebFetch, line anchors, output capping
  permissions/  rule engine, shell decomposition, Seatbelt/bubblewrap
  hooks/        the four events, the trust boundary, subprocess execution
  skills/ agents/ mcp/
  keys.py       keybinding registry: ids, defaults, descriptions, user overrides
  term/         the renderer: raw mode, input decoding, the differ, the screen
  tui/          views, commands, per-tool renderers, themes
    themes/     the shipped palettes, as JSON

The core is headless and emits events; the TUI and print mode are both just consumers. Nothing under core/, tools/, providers/ or permissions/ imports tui/.


Releasing

For maintainers. HX publishes to PyPI as hx-cli from CI, on a tag.

One-time setup:

  1. Push the repo to github.com/<owner>/hx and update the URLs in pyproject.toml.
  2. Create a GitHub environment named pypi.
  3. On PyPI, add a trusted publisher for the project: owner, repo hx, workflow ci.yml, environment pypi. No API token is stored anywhere.

Each release:

# 1. move CHANGELOG.md's [Unreleased] entries under the new version, dated,
#    and leave a fresh empty [Unreleased] behind
# 2. bump __version__ in src/hx/__init__.py
# 3. uv run pytest tests/e2e/test_cli.py  # the version must have a section
git commit -am "release: X.Y.Z"
git tag v$(uv run hx --version | cut -d' ' -f2) && git push origin main --tags

Step 1 is not optional and is not a convention: tests/e2e/test_cli.py fails the whole suite when __version__ has no CHANGELOG.md section, so a release that skips it cannot get past CI to the publish job. The file ships inside the wheel and hx changelog prints it, so a version with no section there reaches users as a blank release.

The publish job runs only on refs/tags/v* and only after lint, the test suite and an install-script run in a clean Debian container have passed. It builds with uv build and uploads via OIDC.

To check a build before tagging:

uv build && ls dist/

Status

Feature complete against the original plan: the agent loop, OpenRouter streaming with prefix caching and accurate cost accounting, session persistence and resume, the tool suite, the permission engine and OS sandbox, late injection, compaction, output capping, skills, subagents, MCP, and the TUI.

Three routes to a model: an OpenRouter API key, a ChatGPT Plus/Pro subscription over OAuth, or a Devin subscription over OAuth. The model id decides which.

Published to PyPI as hx-cli, released from CI on a tag.

The one thing still unproven in CI is a live call on any route: the live tests exist and cover all three wire formats, tool use, reasoning replay and a genuine cache hit, but they need a credential and are deselected by default.

Metadata

Release files for hx-cli 0.2.16

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hx-cli 0.2.16
File Size Uploaded
hx_cli-0.2.16.tar.gz 642.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for hx-cli 0.2.16
File Interpreter ABI Platform
hx_cli-0.2.16-py3-none-any.whl Python 3 none any Details

Total release size: 1.2 MB

Release files / hx_cli-0.2.16.tar.gz

Download URL hx_cli-0.2.16.tar.gz
Size 642.9 kB
Tags Source
SHA-256 checksum
How to use checksums
4e802d1f8eec489442d73cf11365cb2e3281e7708cd887b83bdeacccbc7ba81c
BLAKE2b-256 checksum
How to use checksums
3f23f1a8cc8dc170b12accb63b5e15b14cb68e55df0c05109519ab1fe87d112e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.

Transparency log

Release files / hx_cli-0.2.16-py3-none-any.whl

Download URL hx_cli-0.2.16-py3-none-any.whl
Size 544.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8f20e310970f02357a457978c403d9e629c78d42caef6acdd707993a2f8134a8
BLAKE2b-256 checksum
How to use checksums
d9d9bbf97fc5992eb849c6be7573cb8768c6dde11f26c9c6294f53871c226619
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.18

2 release files

0.2.17

2 release files

This release

0.2.16 This release

2 release files

0.2.10

2 release files

0.2.9

2 release files

0.2.8

2 release files

0.2.7

2 release files

0.2.6

2 release files

0.2.5

2 release files

0.2.4

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.2

2 release files

0.1.0

2 release 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