agent harness for the terminal

    __  __  _  __
   / / / / | |/ /
  / /_/ /  |   /
 / __  /  /   |
/_/ /_/  /_/|_|

The session is still there after you quit.

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

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

MITPython 3.11+macOS and Linuxv0.2.2

02 — the session

It draws into your scrollback.

One turn, start to finish: a file reference, a couple of tool calls, a command it is not allowed to run, and a status bar that never stops telling you what this is costing.

hx · ~/project
~/project (main) default · sandbox seatbelt
0 0 cache - $0.00 ▱▱▱▱▱▱ 0/200k anthropic/claude-sonnet-4.5
esc interrupt · ctrl+c clear · ctrl+d exit · ctrl+o expand · ctrl+p palette · ctrl+l model · / commands · ! bash · @ files

Nothing was repainted. Nothing was erased.

03 — models and routes

Routing is the model ID. That's the whole mechanism.

Three routes. Which one serves a turn is decided by the prefix on the model id alone, so "what paid for that" is always answerable by reading it.

How hard it thinks

/effort — clamped down to what the model actually advertises the status bar appends it: gpt-5.6-terra (sub) · high

On purpose

There is no /route command and no auto-selection. Routing is a prefix on the model id; a second concept would be a second thing to be wrong about.

On purpose

There is no Claude Pro/Max route. Anthropic rejects OAuth tokens unless the request impersonates Claude Code, which HX will not do. Claude models stay available through OpenRouter.

04 — safety, two layers

Deny beats ask beats allow. Even in bypass mode.

Rules decide what may be attempted. The OS sandbox decides what is reachable if something gets through anyway.

Bash(git status && rm -rf /)
  • git statusallowed
  • rm -rf /ask

Shell commands are decomposed into their real segments, so a dangerous tail cannot ride in on an allowed head. It is argument-aware too: sed -i, find -exec and git branch -D still ask.

On purpose

With no sandbox backend on the machine, the status bar says no-sandbox rather than implying protection that is not there.

Precedence

deny> ask> allow
  • Rules are Tool(specifier) strings: Bash(git commit:*), Read(**/.ssh/**).
  • A deny holds in every mode, bypass included.
  • "Always allow" writes a rule to ~/.hx/projects/<project>/settings.local.json.

The OS sandbox

  • Seatbelt on macOS, bubblewrap on Linux.
  • Writes confined to the project directory and temp.
  • ~/.ssh, ~/.aws and HX's own auth.json are unreadable from inside it.
  • Outbound network is off unless permissions.allow_network opens it.
  • Every piece of untrusted text is escape-sanitized, so a file you read cannot set your clipboard, rename your window or erase the screen.

Modes

  • plan
  • default
  • acceptEdits
  • bypass

shift+tab cycles. In plan, mutating tools are not offered to the model at all - it is not asked to behave, it cannot.

05 — context engineering

Long sessions stay cheap.

The prefix is stable so the cache stays warm. Everything volatile is injected late, onto the newest message, where it cannot move the breakpoint.

system + tools — cached
history — cached to breakpoint
newest turn + late injection

▾ breakpoints [static, rolling] CH 78% — a rate that collapses after an edit is the visible symptom of a broken prefix

Late injection

Todos, the git branch and stale-file notices land on the newest user message, with stale copies stripped. The cached prefix never moves.

Compaction at 80%

Snaps to turn edges. Recent turns and todos survive verbatim; superseded messages are flagged, not deleted.

Output capping

25k chars or 2000 lines. Head and tail are kept, the rest spills to the session directory and the model is handed the path.

/rewind

Restores files from a content-addressed snapshot taken before every write. It reports the files it did not restore because something outside changed them, leaves shell side effects alone, and deliberately does not rewind the cost ledger - you still spent that.

06 — the prompt line

Three prefixes and a key.

Most of the interface is one input line. What you type first decides what it is.

commands

Commands. Everything the session can be told to do about itself, rather than about your code.

  • /model [query]
  • /models refresh
  • /effort [level]
  • /configure
  • /login [provider]
  • /logout <provider>
  • /mode [mode]
  • /permissions
  • /context
  • /cost
  • /compact [focus]
  • /clear
  • /resume
  • /rewind
  • /title [text]
  • /prompt
  • /todos
  • /skills
  • /agents
  • /mcp
  • /theme [name]
  • /queue [steer n]
  • /copy
  • /fullscreen [on|off]
  • /mouse [on|off]
  • /init
  • /help
  • /quit

Files. Completion over the project, so a path goes into the prompt exactly as it is on disk.

  • src/hx/core/context.py
  • src/hx/core/compaction.py
  • src/hx/core/sessions.py
  • tests/test_context.py

Shell. Runs the command directly, without a model turn. It skips the model, not the safety layers - the rules and the sandbox still apply.

  • ! git status -sb
  • ## fix-upload-flake...origin/main
  •  M src/hx/tools/bash.py

enter mid-turn

Queues. The turn finishes, then yours runs. /queue shows what is waiting; /queue clear drops it.

alt+enter mid-turn

Steers. The model call is cut mid-stream and your message goes in instead. Tools already running are allowed to finish.

ctrl+p palette ctrl+l model picker ctrl+o expand tool output shift+tab cycle modes all rebindable in ~/.hx/keybindings.json

07 — extending it

Everything you add is a file on disk.

Project copies live in ./.hx/ and shadow your own in ~/.hx/. Check them in, or don't.

Skills

.hx/skills/<name>/SKILL.md

Only the name and description sit in context. The body loads when the model calls Skill(name), and allowed-tools can narrow the toolset while it is active.

---
name: deploy
description: Tag, build and ship a release
allowed-tools: Read, Bash
---

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

Subagents

.hx/agents/<name>.md

Built in: explore, plan, general. A subagent never receives Task, so it cannot recurse. Give it its own model if you like.

---
name: reviewer
description: Reviews a diff against conventions
tools: Read, Grep
model: openai/gpt-5
---

You review code. Cite file:line.

MCP

.hx/mcp.json

stdio and HTTP transports, connected concurrently with a per-server timeout. A broken server is dropped with a warning instead of taking the session down.

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

Themes

~/.hx/themes/<name>.json

Every role is required - a missing one is an error, not a silent black. The role names are pi's, so pi theme files load here unchanged.

{
  "name": "mine", "dark": true,
  "vars":   { "green": "#b5bd68" },
  "colors": { "background": "#18181e",
              "success": "green" /* a vars key */ }
}

08 — fits your machine

Layered config. Nothing written into your repo.

Precedence, highest first

  • 1 CLI flags
  • 2 HX_* environment
  • 3 ~/.hx/projects/<project>/settings.local.json
  • 4 ./.hx/settings.json
  • 5 ~/.hx/settings.json

… and defaults underneath

Permission rule lists and prompt.append are unioned across layers. Everything else is replaced.

On purpose

HX never writes into your repo. ./.hx/settings.json is read-only to it, and "always allow" lands in ~/.hx/ instead.

The command surface

hxinteractive TUI in the current directory
hx -p "…"headless; assistant text to stdout, tool activity to stderr
hx resumeresume the last session here, or one by id
hx promptprint the system prompt this directory would use
hx mcplist, add and remove MCP servers
hx authwhich routes have a credential, and where from
hx docsthe manual, or one section of it
hx changelogwhat shipped in each version
hx upgradeupdate to the latest release

--model  --mode  --cwd  --no-sandbox  --system-prompt  --append-system-prompt

hx docs and hx changelog ship inside the wheel. They answer from the version you actually have installed, offline, and the system prompt points the agent at them instead of its own recollection.

09 — install

Sixty seconds.

macOS and Linux, Python 3.11 or newer.

STEP 01

Install

The script bootstraps uv if you don't have it, pins a Python, and is safe to re-run - it upgrades in place.

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

Bootstraps uv, pins Python 3.12, installs HX as an isolated tool.

$ uv tool install hx-cli

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

$ pipx install hx-cli

Same isolated-tool arrangement, if pipx is what you already use.

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

Runs straight from the checkout.

Command not found? export PATH="$(uv tool dir --bin):$PATH"

STEP 02

Connect a model

One credential for one route. They live in ~/.hx/auth.json, mode 0600, one entry each.

$ hx auth set # paste the key, hidden input

On first run HX asks for one. Get a key at openrouter.ai/keys. Pay per token.

$ hx auth login openai-codex

Needs a paid ChatGPT plan. Opens auth.openai.com and redirects to a loopback listener; over SSH, paste the final URL back, or set HX_LOGIN_DEVICE_CODE=1.

$ hx auth login devin

Needs a Devin subscription. Enterprise tenants sign in against their own API server.

Optional: hx auth set tavily turns on WebSearch and WebFetch.

STEP 03

Run it

Inside a project directory. That is the whole setup.

$ cd ~/your-project
$ hx

Then /model to pick one, shift+tab to change permission mode, and /init to write an AGENTS.md for the project.

Update later with hx upgrade.

Read this one

The PyPI package is 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.

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

10 — reference

Everything else.

Tools

  • Bash persistent shell; cd and exports carry over
  • BashOutput / KillShell background jobs
  • Read numbered, offset + limit
  • Write refuses to clobber an unread file
  • Edit exact string, replace_all, atomic multi-edit
  • Glob newest-mtime first
  • Grep files / content / count
  • TodoWrite · Task · Skill
  • WebSearch / WebFetch only registered when a Tavily key resolves
  • mcp__<server>__<tool>

CLI

  • hx interactive TUI
  • hx -p, --print headless, pipeable
  • hx resume [id]
  • hx prompt
  • hx mcp list | add | remove
  • hx auth [set | clear | login | logout]
  • hx docs [section | --all]
  • hx changelog [version | unreleased]
  • hx upgrade · --version
  • flags: --model --mode --cwd --no-sandbox --system-prompt --append-system-prompt

Keys

  • enter send · ctrl+j newline
  • esc interrupt · ctrl+d exit
  • shift+tab cycle permission mode
  • ctrl+p palette · ctrl+l model
  • ctrl+o expand output · ctrl+t plan
  • ctrl+x copy message · ctrl+c copy selection
  • ctrl+up / ctrl+down prev / next message
  • pgup / pgdn · ctrl+home / ctrl+end
  • alt+enter steer · tab completion
  • readline: ctrl+a/e/w/u/k/y, alt+b/f/d, alt+y kill ring, ctrl+_ undo

Environment

  • HX_MODEL · HX_SUBAGENT_MODEL
  • HX_MAX_TOKENS · HX_COMPACT_AT
  • HX_PERMISSION_MODE · HX_SANDBOX
  • HX_THEME · HX_QUIET_STARTUP
  • HX_GIT_NOTICES · HX_HOME
  • HX_OPENROUTER_API_KEY, then OPENROUTER_API_KEY
  • HX_TAVILY_API_KEY · HX_DEVIN_API_KEY
  • HX_CA_BUNDLE also SSL_CERT_FILE, REQUESTS_CA_BUNDLE
  • HX_LOGIN_DEVICE_CODE headless OAuth
  • the environment wins over the saved file

Python 3.11, 3.12, 3.13, 3.14 Platforms macOS, Linux Sandbox Seatbelt, bubblewrap License MIT Version v0.2.2