Help us shape the future of Go! Take the 2026 Go Developer Survey

hand

module
v0.3.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 10, 2026 License: MIT

README

Hand

Hand is a minimal, extensible terminal coding agent. Point it at a directory and it can read, search, edit, and run commands in that workspace on your behalf — with a lightweight approval gate for anything that changes files or runs shell commands.

Hand is deliberately small: one binary, one config file, no daemon, no project scaffolding forced onto your repo. It's built on top of harness, a Go library for building LLM agents.

Current release: v0.3.2. This release pins Harness v0.4.1 and supports macOS and Linux on AMD64 and ARM64. Hand's interfaces remain pre-1.0; pin the release you deploy and follow the migration and rollback guide when upgrading.

What's in v0.3.2

  • Direct persistent Bash approval: /permissions allow bash --project.
  • Inspect saved grants with /permissions; revoke with /permissions revoke project-bash.
  • Clearer approval labels distinguish the exact command from project-wide Bash approval.

What's in v0.3.1

This patch fixes repeated tool-call finish frames causing cancelled runs, preserves meaningful protocol errors, and avoids preventive compaction when conversation history is too short. Default OpenRouter profiles now discover exact model context limits from the public catalogue and cache them for fifteen minutes. Explicit limits take precedence; unavailable metadata retains the labelled conservative fallback, inspectable with /model.

What's in v0.3.0

v0.3.0 turns Hand from a compact interactive agent into a fuller coding-agent runtime while retaining a single native CLI:

  • Explicit host or Docker-container tool execution. Container mode uses an immutable image and worker digest, an allowlisted environment, explicit mounts and limits, and never falls back to host execution.
  • Durable sessions with resume, naming, branching, export, attachments, checkpoints, restore previews and interrupted-operation recovery.
  • Scoped, revocable permissions; mandatory verification profiles; Stop-hook goal loops; token, cost, run and wall-clock budgets.
  • Mid-run steering and follow-up queues, background processes, full output artifacts, context pins and inspectable agent state.
  • Named provider profiles with explicit context, output, reasoning and input capabilities, plus verified local-model metadata discovery.
  • Versioned JSONL and RPC automation, an embeddable Go SDK, and reviewed, digest-pinned packages and extensions.

Container isolation applies to commands run through the tool backend. Local or remote MCP servers, hooks and explicitly approved host extensions execute outside that container; Hand requires those boundaries to be acknowledged in container mode. See Execution isolation.

Installing

The CLI is a native Go executable. Distribution archives also include a Linux tool worker for optional container execution. Shell tools and optional servers/extensions have their own runtime requirements.

Build from source

You need Go 1.25.1+.

git clone https://github.com/sausheong/hand.git
cd hand
make build          # writes bin/hand
./bin/hand

Or install straight into $GOPATH/bin (or $GOBIN) without cloning:

go install github.com/sausheong/hand/cmd/hand@latest
hand

Make sure that directory is on your PATH.

Download a prebuilt binary

Choose an archive and SHA256SUMS from the same release. Supported targets are darwin-amd64, darwin-arm64, linux-amd64 and linux-arm64. Follow installation, upgrades and rollback to verify the checksum before extraction, preserve the complete CLI/worker installation, and back up existing sessions before upgrading.

Setting up an API key

Hand doesn't store API keys — each provider reads its key from its own environment variable:

Provider Environment variable
Anthropic ANTHROPIC_API_KEY
OpenAI OPENAI_API_KEY
Gemini GEMINI_API_KEY
OpenRouter OPENROUTER_API_KEY
LiteLLM LITELLM_API_KEY (optional — many self-hosted proxies don't enforce auth)
Local none — local servers don't authenticate requests

Export the one matching your default model before running Hand, e.g.:

export ANTHROPIC_API_KEY=sk-ant-...

LiteLLM and OpenRouter are aggregators, not single vendors — each fronts dozens to hundreds of underlying models behind one OpenAI-compatible endpoint, so supporting them once gets you that whole catalog instead of a one-off integration per vendor. OpenRouter needs no other setup (--model openrouter/<catalog-id>); LiteLLM is self-hosted, so you also need --base-url (or base_url in config) pointing at your proxy — there's no public default endpoint to fall back to.

Watch the prefix. OpenRouter's own catalog ids look like anthropic/claude-sonnet-4-5 or google/gemini-3-pro — a vendor name followed by a model name, the same shape Hand uses for provider/model. The first segment you give Hand always selects Hand's own provider, so to pick that catalog entry through OpenRouter you must repeat the aggregator prefix: --model openrouter/anthropic/claude-sonnet-4-5 (or /model openrouter/anthropic/claude-sonnet-4-5 at the prompt). Drop the openrouter/ and Hand switches straight to its own anthropic provider — a real vendor, so it won't be rejected as "unknown provider" — and calls the real Anthropic API directly with whatever model string followed, which usually isn't a valid Anthropic model id.

Local models run on your own machine — no key, no network round-trip beyond it. --model local/<model-name> talks to http://localhost:11434/v1 by default (Ollama's endpoint — e.g. local/qwen2.5:3b, matching what ollama list shows); pass --base-url for a different port or a different local server entirely (LM Studio, llama.cpp's server, vLLM, or anything else exposing an OpenAI-compatible endpoint).

Running it

Run hand from the directory you want it to work in — that directory is the workspace, and every file tool is confined to it:

cd my-project
hand

This opens an interactive terminal UI. Type a request and press Enter; Hand streams its response and shows each tool call it makes along the way. Assistant responses are rendered as Markdown (headings, bold/italic, lists, code blocks) — the model's raw formatting shows up styled, not as literal **/#/` characters. The style defaults to dark; see --markdown-style below to change it.

One-shot mode

For scripting or CI, run a single turn non-interactively with -p:

hand -p "list all TODO comments in this repo"

By default, any gated tool call (see below) is denied in one-shot mode rather than prompting — there's no one to ask. Pass --yes to auto-approve everything for that run instead:

hand -p "fix the failing test in pkg/foo" --yes

One-shot exit codes are stable for scripts:

Code Meaning
0 Answer completed; all configured mandatory Stop validators passed
2 Invalid invocation or configuration
3 Required prompt/Stop validation failed
4 Tool-turn or goal-iteration limit exhausted
5 Provider, runtime or infrastructure failure
130 Interrupted or cancelled

A normal answer without mandatory validators is not verified coding success. Passing validators establishes only what those checks actually test. Reaching --max-turns or --max-iterations never returns success. SIGINT and SIGTERM cancel an active one-shot run, drain its events and allow runtime cleanup before exit, including during MCP startup.

Flags
Flag Description
--model provider/model to use for this run, e.g. anthropic/claude-sonnet-5 — overrides ~/.hand/config.json
--profile Named provider/model profile from config
--base-url Custom API base URL — required for litellm, optional for openai/openrouter, not supported for gemini
--context-limit Explicit active context limit for this invocation
--reasoning off, low, medium or high; the selected profile must declare support
--max-turns Cap the agent's tool-use loop for this run (default: 50, or max_turns in config)
--fallback-model provider/model to retry against on a transient provider error, same provider as --model
--markdown-style Glamour style for rendering assistant Markdown: dark, light, ascii, notty, pink, dracula, tokyo-night (default dark) — overrides ~/.hand/config.json
--compaction-threshold Fraction (0-1] of the context window that triggers preventive compaction (default 0.4) — overrides ~/.hand/config.json
--max-iterations Cap how many turns a Stop-hook goal loop may chain automatically (default 10, or max_goal_iterations in config)
--new-session Create a new session while preserving existing history
--session Resume a saved session by stable ID
--export-session Export a stopped session to a new JSONL path without starting a model
--checkpoint-dir Store bounded run checkpoints in a private directory outside the workspace
--rpc Serve the versioned JSONL RPC protocol on stdin/stdout
--jsonl Emit versioned JSONL events in one-shot mode
-p "<prompt>" Run one turn non-interactively and exit (no TUI)
--yes Auto-approve all gated tool calls for this run (only valid with -p)

Slash commands

Inside the interactive UI, a message starting with / runs a command instead of being sent to the model. Typing / shows an auto-complete dropdown (arrow keys to move, Tab or Enter to fill it in).

Area Commands
Help and display /help, /clear, /output, /exit (/quit)
Models and context /model, /profile, /context, /compact, /summarizer, /summarizer-confirm, /summarizer-follow, /pins, /pin, /unpin
Sessions /new, /resume, /name, /tree, /fork, /export
Work in progress /steer, /followup, /queue, /process, /state
Safety and evidence /boundary, /permissions, /changes, /restore-preview, /restore-confirm, /restore-cancel, /recoveries, /recovery-resolve, /verify, /verify-confirm, /verify-check, /verify-list, /verify-delete
Usage and budgets /usage, /budget, /budget-tokens, /cost, /prices, /prices-review, /prices-confirm, /run-budget, /time-budget
Integrations /mcp, /skills, /reload, /extension

Run /help for exact arguments. Commands that mutate durable state use review, digest confirmation or explicit acknowledgement rather than applying an unseen change immediately.

Manual /compact runs in the background while the terminal remains responsive. The footer shows its progress; Ctrl+C requests cancellation. New turns and session/model changes wait until the compaction worker has stopped. /quit cancels an active compaction and exits after the worker returns.

Conversations are saved per-workspace, so quitting and re-running hand in the same directory picks up where you left off — including across a /model switch, so relaunching hand does not by itself give you a clean slate.

After /model, a long-lived conversation can still "remember" the old model. Hand tells the model which one it's running as fresh on every turn, but if the conversation already has several turns where the model stated its old identity (e.g. you asked "which model are you" a few times before switching), it can keep repeating that instead of the current, correct answer — a model generally treats what it already told you as stronger evidence than an instruction to disregard it. This isn't a bug in the switch itself: the request really is going to the new model, it's just the model's self-report that lags. /new starts a blank conversation with no such history to compete with, which reliably clears it up.

Status line

The line just above the input box is always showing, whether or not a turn is running. While idle, after a turn has completed at least once:

ready   anthropic/claude-sonnet-5  ·  ctx 38.4k/200k (19%)  ·  turn 6.1k tok  ·  session 21.9k tok  ·  last turn 5.8s

While a turn is in progress:

⠋ working... 4.2s · 3 tool calls   anthropic/claude-sonnet-5  ·  ctx 38.4k/200k (19%)  ·  turn ~1.2k tok  ·  session 21.9k tok
  • Run state — ready, or a spinner with a live elapsed-time counter while a turn is in progress, plus a running count of tool calls made so far this turn (shown once at least one has run) — concrete evidence of progress on a turn that's mostly tool calls with no text in between, where the token figures below don't move at all until the turn finishes.
  • The active model — the current provider/model string. Shown here, not just in the startup banner, because /model can change it mid-session and the banner scrolls out of view — this is the one place that always reflects what's actually running right now.
  • ctx — how much of the active model's context window the last completed turn's request used, and the window size itself (e.g. 38.4k/200k (19%)). Turns red once usage crosses 85% of the window, a nudge that compaction (automatic, or /compact) is worth watching for.
  • turn — the most recently completed turn's total tokens (input + output). While a turn is running, shows a live ~-prefixed estimate of the in-progress response instead (token usage is only reported once a turn finishes, so this is approximate, not exact).
  • session — the running total across every turn since this hand process started (resets on restart — it isn't persisted with the saved session).
  • last turn — how long the most recently completed turn took (hidden while idle before any turn has completed, and while a turn is running — see the live elapsed-time counter in the run state instead).

/usage prints the same figures (plus the raw input/output/cache breakdown) as a one-off transcript entry, if you want it in the scrollback.

Tool output shown under a ✓/✗ line is a short preview — 5 lines / 500 characters — not the full result. Scroll up (pgup/pgdown, ctrl+u/ ctrl+d) to review earlier output; the preview cap keeps the transcript itself scannable rather than a full pager for every command.

Hand doesn't capture the mouse, so your terminal's normal text selection/copy works exactly as it would anywhere else.

Search results include a completeness footer. The search tool streams large files and honours workspace/nested .gitignore rules; include_ignored and include_hidden broaden that scope explicitly. Git metadata, binary content and non-regular entries are excluded and counted. Read errors, lines exceeding 1 MiB, cancellation and result/output limits are reported explicitly. An incomplete search cannot establish that a match is absent. Git and rg are not required. The default output budget is 64 KiB, configurable up to 256 KiB.

Execution isolation

Host mode is the default and is labelled as unrestricted host execution. To run shell tools and compatible reviewed extensions inside a container, configure a local Docker engine, a previously pulled immutable image, and the matching hand-tool-worker-linux shipped in the release archive:

"execution": {
  "backend": "container",
  "docker": "/absolute/path/to/docker",
  "socket": "/absolute/path/to/docker.sock",
  "image": "sha256:<64 hex digits>",
  "worker": "/absolute/path/to/hand-tool-worker-linux",
  "worker_sha256": "<digest from WORKER.json>",
  "writable": false,
  "network": false
}

The container receives only its declared workspace and declared resources. Writes and network access are off unless enabled. Image and worker identities must be immutable, paths must be absolute, and an unavailable backend is an error—Hand does not retry the command on the host. /boundary reports the effective trust boundary.

MCP servers and hooks are integration processes or remote services other than the containerized tool worker. If either is configured with container mode, startup fails until trust_external_mcp or trust_external_hooks is explicitly set for the relevant boundary. These acknowledgements expose the boundary; they do not move those integrations into the container or reduce their operating-system authority.

Approval prompts

Hand gates bash, write_file, edit_file, persistent todo_write, mutating skill_manage operations, unknown tools, and tools from untrusted MCP servers. Each prompt reads Allow <tool>? [y]es / [a]lways / [n]o:

  • [y]es — run just this call
  • [a]lways — never ask again for this tool in this workspace (persisted to .hand/settings.json in the workspace)
  • [n]o — deny the call

If a workspace's .hand/settings.json already grants "always allow" for something (e.g. it was committed to a repo you just cloned), Hand asks you to confirm trusting it once per workspace before honoring those entries — add .hand/ to your .gitignore if you don't want your own approvals checked into version control.

Extending Hand

Hand has three, orthogonal ways to extend what it can do, and it's worth being precise about what each one actually adds:

  • MCP servers add new tools the model can call.
  • Skills add new knowledge the model can pull into context on demand — no new tools, just a name/description index plus a way to fetch the body.
  • Hooks add lifecycle interception — a chance to observe or block something that's about to happen, for every tool call (built-in, MCP, or skill-related) and every turn, not just gated ones.

They compose: an MCP server's tools are gated and hookable exactly like Hand's own bash/write_file/edit_file; a PreToolUse hook's matcher can target mcp__<server>__<tool> just as easily as bash.

MCP servers

MCP (Model Context Protocol) servers are external processes (or remote endpoints) that expose their own tools — a GitHub server exposing create_issue, a database server exposing query, and so on. Configure them in ~/.hand/config.json's mcp_servers list (see Configuration); there's no per-project MCP config.

Each entry is either:

  • a local command (command, args, env) — Hand spawns it as a subprocess and speaks MCP over its stdin/stdout, or
  • a remote server (url, headers) — Hand connects over HTTP.

Set exactly one of the two per entry.

Connecting. Every configured server is connected synchronously, before the TUI (or one-shot output) appears — a slow server adds visible startup delay, with hand: connecting to N configured MCP server(s)... printed while it waits. Connecting is bounded to 15 seconds total across every configured server; on timeout, Hand exits with an error rather than hanging indefinitely (a server that finishes connecting a few seconds after the timeout is still cleaned up properly in the background, so it won't leak a subprocess).

Naming and gating. Each tool a connected server exposes gets registered into Hand's tool registry as mcp__<server-name>__<tool-name> — visible to the model under that full name. By default, every tool from every server is gated exactly like bash/write_file/edit_file: it triggers the same Allow <tool>? [y]es / [a]lways / [n]o approval prompt (see Approval prompts) before it's allowed to run. Setting "trusted": true on a server's config entry skips the prompt for every tool that server exposes — only do this for a server whose output you trust as much as Hand's own built-in tools, since an untrusted MCP server can expose arbitrary tools you've never seen before that might use identical names to a trusted one (Hand resolves the longest matching configured server name first specifically to avoid a shorter trusted name accidentally covering a longer untrusted one that happens to share a prefix).

"mcp_servers": [
  { "name": "github", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_TOKEN": "..." }, "trusted": false },
  { "name": "remote-example", "url": "https://example.com/mcp", "headers": { "Authorization": "Bearer ..." }, "trusted": true }
]
Skills

Skills are reusable, on-demand-loaded procedural knowledge: a name and one-line description show up in Hand's system prompt at startup as a "## Skills" index, and the full body only loads into context when the agent actually decides it needs it (via a load_skill tool call) — cheaper than stuffing every skill's full content into the system prompt on every turn, and the index itself is built once at startup and reused unchanged for the life of the process.

Where they live. Hand reads two directories and merges them, project winning on a name collision:

  • ~/.hand/skills/ — personal skills you maintain by hand, shared across every project.
  • <workspace>/.hand/skills/ — project-specific skills. Committable to a repo alongside HAND.md/AGENTS.md, so a team can share "how we do things here" knowledge the same way they share project instructions.

Format. Each skill is its own directory containing SKILL.md, with a frontmatter block for metadata and everything after it as the body the model sees once loaded:

.hand/skills/run-tests/SKILL.md
---
description: how to run this repo's test suite
---

Run `make test`. Integration tests additionally need a running Postgres —
see docker-compose.yml.

How a skill actually gets used in a turn: at startup, Hand scans both directories, builds the merged index, and injects it into the system prompt. Mid-conversation, if the model decides a skill looks relevant to what you asked, it calls load_skill with the skill's name; Hand returns the full SKILL.md body as that tool call's result, which then sits in context for the rest of the conversation like any other tool output (subject to the same compaction as everything else — see compaction_threshold below).

Self-authoring. The model can also create, patch, replace, remove, list, or get skills itself via a skill_manage tool — useful for "remember how to do this" requests, where the agent writes its own procedural knowledge for next time. Self-authored skills always land in the project-local store (<workspace>/.hand/skills/), never your personal ~/.hand/skills/, so an agent-authored skill stays scoped to the repo it was learned in rather than silently spreading across every project you use Hand in. load_skill and the skill_manage get/list operations are read-only and do not prompt. Create, patch, replace and remove require approval, including in one-shot mode. The index refreshes for subsequent model requests after self-authoring. Use /reload while idle to refresh external skill edits.

Run /skills to see what's currently loaded, from both directories.

Installed package skills can be selected explicitly with --package-skills. Their load_skill results include source provenance and support bounded, hash-verified reads of declared relative text resources. See installed package skills for selection and usage.

Hooks

Hooks are shell commands Hand runs at five points in the agent loop — before a tool call, after a tool call, at session start, when you submit a prompt, and when a turn stops — configured globally in ~/.hand/config.json's hooks list (see Configuration). Unlike skills, there is no per-project hooks file: a hook executes an arbitrary command, so it gets the same trust tier as an mcp_servers entry (also global-only) rather than something a cloned repo could ship and have run automatically the first time Hand touches it.

Every hook receives a versioned JSON object on stdin:

{"version":1,"event":"PreToolUse","workspace":"/work/project","tool_name":"bash","tool_input":{"command":"go test ./..."}}
Event Fires Can block? JSON payload fields
PreToolUse Before a tool call Yes tool_name, tool_input
PostToolUse After a tool call Observe-only tool_name, tool_input, tool_result, tool_error
SessionStart When a session begins Observe-only Common metadata
UserPromptSubmit Before sending the user's message Yes prompt
Stop After a turn ends Yes — see Goal loop stop_reason, goal_iteration

tool_input is a JSON object; prompt, output and error fields are strings. Empty optional fields may be omitted. Small metadata environment variables remain available: HAND_HOOK_VERSION, HAND_HOOK_EVENT, HAND_WORKSPACE, HAND_TOOL_NAME, HAND_STOP_REASON and HAND_GOAL_ITERATION where applicable.

Migration: scripts reading HAND_PROMPT, HAND_TOOL_INPUT, HAND_TOOL_RESULT or HAND_TOOL_ERROR should read the corresponding JSON stdin fields instead. Temporarily setting "legacy_env": true exports these fields too, but rejects any environment value larger than 16 KiB. It never silently truncates them. The default stdin protocol supports large payloads without relying on operating-system argument/environment limits.

Ordering relative to gating. PreToolUse hooks run for every tool call, not just gated ones — read_file and web_search trigger matching hooks too, even though neither ever shows an approval prompt. For a gated tool, a PreToolUse hook runs before the interactive approval prompt, so a hook can auto-deny a call before you're ever asked about it.

Exit codes and failure policies:

  • Exit 0 allows the operation or passes the validator.
  • Exit 2 denies PreToolUse, aborts UserPromptSubmit, or requests another goal iteration for Stop. Stop uses stdout, then stderr, as its next prompt.
  • Other nonzero exits, spawn errors, timeouts and excessive output fail validators closed by default ("failure_policy": "deny"). A failed Stop validator returns an error, so one-shot mode cannot report success.
  • Optional observers may set "failure_policy": "warn" to log failures and continue. This is the default for observe-only PostToolUse and SessionStart; these events reject "deny" because they cannot veto work. Cancellation stops validation even with "warn".

Timeout defaults to 30 seconds (timeout_seconds overrides it). stdout and stderr capture is limited to 64 KiB each while the process runs; exceeding that limit is a validation failure. On macOS and Linux, cancellation joins the hook's process group and bounded pipe draining before the run settles.

Existing validator configurations now fail closed on script errors. To retain legacy warning-only behaviour for an optional script, explicitly select "failure_policy": "warn". Use a blocking event for mandatory validation.

A matcher (exact tool name, or "*"/omitted for every tool) restricts a PreToolUse/PostToolUse hook to specific tools — including MCP ones, e.g. "matcher": "mcp__github__create_issue". SessionStart/UserPromptSubmit/ Stop have no tool to match against, so matcher is ignored for them.

Two worked examples:

{ "event": "PreToolUse", "matcher": "bash", "command": "./scripts/check-command.sh" }

check-command.sh reads tool_input from its JSON stdin payload (containing the shell command), and exits 2 with a reason on stderr to block anything it doesn't like — e.g. a company policy against curl | sh.

{ "event": "Stop", "command": "notify-send", "args": ["hand finished"] }

A simple desktop notification every time a turn ends, regardless of how it ended (HAND_STOP_REASON is one of completed, max_turns, error, or aborted) — no blocking, just an observation.

Goal loop

A Stop hook can do more than notify — it can tell Hand the goal hasn't been met yet, and Hand will automatically run another turn to keep working, without you re-typing anything. This is what makes an unattended "keep fixing until make test is green" run possible, in both -p one-shot and the interactive TUI.

It reuses the exact same exit-code contract as everything else: your Stop hook exits 0 when the goal is met (stop as normal — the ordinary, backward-compatible case, like the notify-send example above), or exits 2 when it isn't, printing what's still wrong to stdout (falls back to stderr, then to a fixed message if both are empty) — that text becomes the next prompt Hand runs automatically:

{
  "event": "Stop",
  "command": "sh",
  "args": ["-c", "if make test; then exit 0; else exit 2; fi"]
}

A rough sketch: exit 0 if make test passes, otherwise print the failure output and exit 2 — Hand then runs another turn with that output as the prompt, and repeats until the tests pass or the loop gives up.

The TUI displays one final result for each user request, including its automatic continuations. It distinguishes completion, verification failure, exhausted limits, cancellation and execution failure. A completed answer explicitly states whether configured mandatory checks passed. The final allowed iteration still runs validation: it may succeed at the cap, but a request for another iteration reports a limit failure.

In the interactive TUI, an auto-continued turn shows up with a distinct ↻ continuing (goal loop N/max): ... transcript line, never rendered like something you typed. Pressing ctrl+c during an auto-continued turn stops the loop as well as the turn — it does not immediately try again.

Safety cap. max_goal_iterations (config.json) / --max-iterations bounds how many turns a single goal-loop chain may run automatically (default 10, the first turn plus up to 9 continuations) before Hand gives up regardless of what the hook says — the backstop against a broken or malicious goal-check script looping forever and burning API tokens with no one approving the overall goal each time. This is a different knob from max_turns, which bounds the tool-use loop within one turn, not how many turns get chained.

What's unaffected. Per-tool-call approval gating inside each turn is completely unchanged — bash/write_file/edit_file and untrusted MCP tools still prompt (or run their own PreToolUse hooks) exactly as before. The goal loop only decides whether to start another turn once one ends; it has no effect on what happens inside a turn. An errored turn (EventError) never triggers a continuation, regardless of what a Stop hook would have said.

Configuration

Hand reads ~/.hand/config.json on startup (created with defaults on first run):

{
  "model": "anthropic/claude-sonnet-5",
  "default_profile": "primary",
  "profiles": {
    "primary": {
      "provider": "anthropic",
      "model": "claude-sonnet-5",
      "credential_env": "ANTHROPIC_API_KEY",
      "input_types": ["text", "image"],
      "context_limit": 200000,
      "max_output": 64000,
      "reasoning": "medium",
      "reasoning_levels": ["off", "low", "medium", "high"]
    }
  },
  "base_url": "",
  "max_turns": 50,
  "max_goal_iterations": 10,
  "fallback_model": "",
  "markdown_style": "dark",
  "compaction_threshold": 0.4,
  "execution": {
    "backend": "host"
  },
  "mcp_servers": [
    {
      "name": "github",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": { "GITHUB_TOKEN": "..." },
      "trusted": false
    },
    {
      "name": "remote-example",
      "url": "https://example.com/mcp",
      "headers": { "Authorization": "Bearer ..." },
      "trusted": true
    }
  ],
  "hooks": [
    {
      "event": "PreToolUse",
      "matcher": "bash",
      "command": "./scripts/check-command.sh",
      "timeout_seconds": 10
    },
    {
      "event": "Stop",
      "command": "notify-send",
      "args": ["hand finished"]
    }
  ]
}
  • model / base_url / max_turns / max_goal_iterations / fallback_model / markdown_style / compaction_threshold are the same values the CLI flags above override for a single run. markdown_style accepts dark, light, ascii, notty, pink, dracula, or tokyo-night; anything else (including glamour's own auto — deliberately not offered, since it detects light/dark by querying the terminal in a way that can corrupt the input box) falls back to dark. max_goal_iterations is unrelated to max_turns — see Goal loop.
  • default_profile selects one entry from profiles. Profiles bind the provider, bare model ID, credential environment-variable name, endpoint and declared capabilities. Hand never stores the credential value. Explicit context and output limits win over verified catalogue/server metadata; unknown limits use a labelled conservative fallback rather than an advertised maximum being guessed as active.
  • compaction_threshold controls how eagerly Hand summarizes older conversation to keep token usage down — preventive compaction fires once the estimated context passes this fraction of the model's context window (default 0.4, i.e. 40%). A tool-call-heavy turn (many file reads, test runs, greps) can otherwise re-send a large tool result on every subsequent call in that same turn until it's compacted away, so lower is more aggressive about controlling token spend at the cost of summarizing sooner (and losing a bit of verbatim detail from earlier in the conversation); higher keeps more raw context around longer. Must be in (0, 1] — anything else falls back to the default.
  • mcp_servers — see MCP servers above for what each field means and how gating/trust works.
  • hooks — see Hooks above for the five events, the exit-code contract, and what each HAND_* environment variable carries.
  • execution — see Execution isolation. host is unrestricted; container is fail-closed and requires pinned runtime inputs.

Development

make build   # build bin/hand
make run     # go run ./cmd/hand, no build step
make test    # go test ./...
make vet     # go vet ./...
make fmt     # go fmt ./...
make install # go install ./cmd/hand

For the offline CI checks, run python3 scripts/validate.py --output /tmp/hand-validation-001 with a new output directory. It inventories tests, checks formatting/build/vet, runs uncached race-enabled tests with coverage, rejects skipped or missing tests, and checks --help/--version without credentials. Raw logs and a JSON report remain in the output directory, including on failure. The v0.3.0 release candidate was also qualified with the retained-scope checker and isolated-agent gateway probes described in docs/acceptance/.

hand --version reports the version and source commit without loading configuration. The Go binary needs no separate language runtime; shell execution requires Bash on macOS/Linux, and optional MCP servers/extensions may require their own runtimes.

Releasing

make dist                      # cross-compile darwin/linux amd64/arm64 into dist/*.tar.gz + SHA256SUMS
make release VERSION=v0.3.0    # tag and push — the step below then runs automatically

Pushing a v* tag triggers .github/workflows/release.yml: it first requires the shared Linux/macOS validation workflow to pass for that commit, then runs make dist, verifies checksums and archive contents, smoke-tests the native binary, and publishes a GitHub release with the resulting archives and checksums attached, via softprops/action-gh-release. The distribution directory must be new; use DIST_DIR to retain prior builds. Every archive includes the Linux worker and its digest manifest, and CI requires worker validation. Publish only a reviewed commit whose branch validation is green; the tag workflow repeats validation before creating the release.

License

MIT — see LICENSE.

Permission migration, revocation and rejected-journal recovery are described in the permission migration guide.

Session navigation, checkpoint previews, interrupted restore recovery and named verification are described in sessions and restore.

For subprocess or embedded integrations, see automation and the Go SDK.

See packages and extensions for installation, activation, updates and rollback.

For common errors and recovery steps, see troubleshooting.

Persistent Bash approval

To allow all Bash commands in the current project, enter this while Hand is idle:

/permissions allow bash --project

The grant persists across restarts for the current configuration. Inspect it with /permissions and remove it with /permissions revoke project-bash. This is a Bash approval preference; execution boundaries still come from the configured backend. The approval prompt's always this command choice only remembers the exact command being shown.

Directories

Path Synopsis
cmd
hand command
Command hand is an interactive terminal coding agent built on harness.
Command hand is an interactive terminal coding agent built on harness.
hand-tool-worker command
hand-tool-worker is a private execution helper.
hand-tool-worker is a private execution helper.
examples
compatibility command
Command compatibility compares Hand interfaces using a local HTTP provider.
Command compatibility compares Hand interfaces using a local HTTP provider.
embedded command
The embedded example opens Hand and lists sessions without a model call.
The embedded example opens Hand and lists sessions without a model call.
extensions/go-task-note command
task-note demonstrates questions, durable host state and editable context.
task-note demonstrates questions, durable host state and editable context.
extensions/go-tool-policy command
tool-policy demonstrates an additional veto for an explicitly named path.
tool-policy demonstrates an additional veto for an explicitly named path.
extensions/go-tool-viewer command
tool-viewer renders an explicitly supplied tool result; it executes no tools.
tool-viewer renders an explicitly supplied tool result; it executes no tools.
extensions/go-verification-hook command
verification-hook checks tracked Git whitespace, not test correctness.
verification-hook checks tracked Git whitespace, not test correctness.
rpc command
The RPC example starts an installed Hand binary and negotiates capabilities.
The RPC example starts an installed Hand binary and negotiates capabilities.
extension
protocol
Package protocol defines the extension JSONL protocol, independently of MCP and Hand's client RPC.
Package protocol defines the extension JSONL protocol, independently of MCP and Hand's client RPC.
internal
agentio
Package agentio wires harness's tool registry, agent spec, and approval gating for Hand.
Package agentio wires harness's tool registry, agent spec, and approval gating for Hand.
app
Package app owns Hand application operations independently of terminal rendering.
Package app owns Hand application operations independently of terminal rendering.
checkpoints
Package checkpoints captures bounded, non-mutating workspace state.
Package checkpoints captures bounded, non-mutating workspace state.
config
Package config reads and writes Hand's user-level configuration file at ~/.hand/config.json.
Package config reads and writes Hand's user-level configuration file at ~/.hand/config.json.
extensions
Package extensions owns extension connections.
Package extensions owns extension connections.
packages
Package packages validates explicitly selected Hand distribution packages.
Package packages validates explicitly selected Hand distribution packages.
permissions
Package permissions manages Hand's project-local "always allow" allowlist at .hand/settings.json.
Package permissions manages Hand's project-local "always allow" allowlist at .hand/settings.json.
rpc
sessionio
Package sessionio maps a Hand workspace directory onto a harness session.Store key, and resolves where sessions live on disk.
Package sessionio maps a Hand workspace directory onto a harness session.Store key, and resolves where sessions live on disk.
toolproxy
Package toolproxy routes admitted built-in requests into the selected backend.
Package toolproxy routes admitted built-in requests into the selected backend.
toolworker
Package toolworker dispatches a single already-admitted built-in operation.
Package toolworker dispatches a single already-admitted built-in operation.
tui
Package tui is Hand's Bubble Tea terminal UI.
Package tui is Hand's Bubble Tea terminal UI.
verification
Package verification records command evidence for a precise captured scope.
Package verification records command evidence for a precise captured scope.
Package protocol defines Hand's versioned JSONL wire messages.
Package protocol defines Hand's versioned JSONL wire messages.
Package sdk provides the versioned Hand RPC client.
Package sdk provides the versioned Hand RPC client.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL