Skip to main content

HX

A terminal coding agent. Python core, Textual TUI, 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.

The API key

On first run HX asks for an OpenRouter key and saves it to ~/.hx/auth.json with mode 0600. Get one at https://openrouter.ai/keys.

To change it later, /configure inside the TUI, or from a shell:

hx auth          # is a key set, and where does it come from?
hx auth set      # paste a new one (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.


Running it

hx                          # interactive TUI in the current directory
hx -p "explain this repo"   # headless: streams to stdout, tool activity to stderr
hx resume                   # resume the last session here
hx resume <session-id>      # resume a specific one
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 mcp list|add|remove      # manage MCP servers
hx auth [set|clear]         # manage the API key
hx upgrade                  # update to the latest release

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

Keys

Key Does
enter send
ctrl+j newline
esc interrupt the current turn
ctrl+c cancel the current turn
shift+tab cycle permission mode
ctrl+p command palette
ctrl+r expand the last tool output
ctrl+t toggle the todo sidebar
ctrl+d quit
@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.

Commands

Command Does
/model [query] pick a model; shows context window, price per Mtok, cache support
/models refresh re-fetch the catalogue
/configure session settings and the API key
/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 reopen a previous session
/todos toggle the sidebar
/skills installed skills
/agents subagent types
/mcp server status
/theme [name] dark, light, ansi
/init generate an HX.md for the project
/help list commands and keys
/quit exit

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_* env  <  CLI flags

Permission rule lists are unioned across layers, so a project can add a deny rule without discarding yours. Everything else is replaced.

{
  "theme": "dark",                    // dark | light | ansi
  "telemetry": false,

  "models": {
    "model": "anthropic/claude-sonnet-4.5",
    "subagent_model": null,           // defaults to "model"
    "max_tokens": 8192,
    "temperature": null
  },

  "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
  },

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

Environment overrides: HX_MODEL, HX_SUBAGENT_MODEL, HX_MAX_TOKENS, HX_PERMISSION_MODE, HX_SANDBOX, HX_COMPACT_AT, HX_THEME. Also HX_HOME to relocate user state.

Where things live

Path What
~/.hx/settings.json your settings
~/.hx/auth.json API key, mode 0600
~/.hx/models.json cached model catalogue, refreshed daily
~/.hx/sessions/ transcripts, spilled tool output, subagent sessions
~/.hx/skills/, ~/.hx/agents/ your skills and agents
./.hx/settings.json project settings, checked in if you like
./.hx/mcp.json project MCP servers
./.hx/skills/, ./.hx/agents/ project skills and agents
./HX.md project instructions, loaded into every session

HX.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.


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 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 — 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.

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.


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. 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.


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

Tests marked live hit the real OpenRouter API and cost money:

OPENROUTER_API_KEY=... uv run pytest -m live

Tests marked sandbox exercise the real OS sandbox and are skipped where no backend exists.

Layout

src/hx/
  cli.py config.py paths.py frontmatter.py
  core/         loop, context assembly, compaction, late injection, sessions, usage
  providers/    OpenRouter, the model catalogue, a scripted provider for tests
  tools/        Bash, Read, Write, Edit, Glob, Grep, TodoWrite, Task, output capping
  permissions/  rule engine, shell decomposition, Seatbelt/bubblewrap
  skills/ agents/ mcp/
  tui/          Textual app, commands, theme, per-tool renderers, widgets

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:

# bump version in pyproject.toml, commit
git tag v0.1.0 && git push origin main --tags

The publish job runs only on refs/tags/v* and only after lint, the test matrix, 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.

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

The one thing still unproven is a live OpenRouter call: the live tests exist and cover the wire format, tool use and a genuine cache hit, but they need a key and are deselected by default.

Download files

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

Source Distribution

hx_cli-0.1.0.tar.gz (277.4 kB view details)

Uploaded Source

Built Distribution

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

hx_cli-0.1.0-py3-none-any.whl (141.1 kB view details)

Uploaded Python 3

File details

Details for the file hx_cli-0.1.0.tar.gz.

File metadata

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

File hashes

Hashes for hx_cli-0.1.0.tar.gz
Algorithm Hash digest
SHA256 d82b55f6cefcfe9670ab6e435ab608794b742dd8374a09ba5b6a75c1c9421ef3
MD5 143e1a4c5ab044dd50b2ec71eed2813c
BLAKE2b-256 05f472e3907b181372f58fd2df90ddd9fa53cb9c9cddae47662111a2df9043c2

See more details on using hashes here.

Provenance

The following attestation bundles were made for hx_cli-0.1.0.tar.gz:

Publisher: ci.yml on aletisunil/hx

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

File details

Details for the file hx_cli-0.1.0-py3-none-any.whl.

File metadata

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

File hashes

Hashes for hx_cli-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d7497dba857ba9e99a9add8b4993a23f8ebbbbd4ad71fc5582deb30535907424
MD5 fad2816829f99bd3a764f3aa8262a78c
BLAKE2b-256 e3be522330097fd9c5edc627113e240bf33da0a0874d8b15a86e6b4eb56bbb57

See more details on using hashes here.

Provenance

The following attestation bundles were made for hx_cli-0.1.0-py3-none-any.whl:

Publisher: ci.yml on aletisunil/hx

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

Release history Release notifications | RSS feed

0.1.8

2 files

0.1.6

2 files

0.1.5

2 files

0.1.4

2 files

0.1.2

2 files

This release

0.1.0 This release

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