Husk
GitHub

Reference

CLI reference

Every command, every flag it actually parses, its exit code, and the flags that exist in the parser but not in --help.

husk <command> [flags]

husk with no arguments prints the top-level help and exits 0 — a question, not a mistake, so probing for the binary in a script does not look like a failure.

husk --help

Global flags

Every command accepts these. Their meaning never changes between commands.

FlagEffect
--jsonMachine-readable output, and the only thing on stdout
-q, --quietErrors only
--no-colorPlain text. Also automatic when piped, and with NO_COLOR
--colorForce colour on, even when piped
--debugFull stack traces, including cause chains
-y, --yesAnswer every confirmation with yes
-h, --helpHelp for any command

--color and -y/--yes are parsed by every command but are only listed in the help for the commands that use them. --version and -V work only on the bare husk invocation, not after a subcommand.

Parsing is strict: a typo'd flag is a usage error, not a silently ignored string.

error unknown flag --jsno -- run with --help to see what this command accepts
hint:  husk help ps

Exit codes

Frozen, because scripts and CI key off them.

CodeMeaning
0Success
1Error
2Usage mistake
130Interrupted (Ctrl-C)

husk exec is the one deliberate exception: it exits with the child command's own code, so husk exec box -- test -f /work/x && echo yes behaves the way you expect.

Command aliases

You typeYou get
ls, listps
createup
destroy, deleterm
shshell
checkdoctor

Computers

husk up

husk up [name] [flags]

With no name, Husk generates one (box-a1b2c). The machine is lazy: nothing runs inside it until you exec.

FlagValueDefault
--providerdocker | podman | local | ssh | flybest available
--flavorbase | python | node | fullbase
--memory2g, 512m, or a plain number of MBprovider default
--cpusa positive numberprovider default
--networknone | egress | fullegress
--persistbooleanoff
--envKEY=VALUE, repeatable
--idle-timeoutseconds900
--jsonprint the ComputerInfo record

--env and --idle-timeout are parsed but are not in husk help up.

A named provider is never silently substituted:

error provider "docker" is not usable: docker is installed but the daemon is not reachable
hint:  start Docker Desktop (or `sudo systemctl start docker`), then re-run `husk doctor`

husk ps

husk ps [--all] [--json]
FlagEffect
-a, --allInclude stopped machines
--jsonPrint the full ComputerInfo records

Piped output is tab-separated with no colour and no truncation, so husk ps | cut -f2 works. An empty list exits 0, not 1.

husk rm

husk rm <name|id> [--all] [--yes]
FlagEffect
--allDestroy every computer
-y, --yesSkip the confirmation
-f, --forceThe same as --yes

Asks first unless --yes, --force or --json is given. On a non-interactive stdin without one of those it refuses rather than guessing:

error refusing to destroy 3 computers without confirmation on a non-interactive stdin

-f/--force is parsed but is not in husk help rm.

husk stop and husk start

husk stop <name|id>
husk start <name|id>

No flags beyond the global set. stop keeps the filesystem:

✓ stopped docsbox — the filesystem is kept; `husk start` brings it back

husk exec

husk exec <name|id> [flags] -- <command...>
FlagValueDefault
--cwdpath inside the machine/work
--timeoutseconds; the process tree is killed120
--envKEY=VALUE, repeatable
--stdintext piped to the command
--jsonbuffer and print the full ExecResultstream

--stdin is parsed but is not in husk help exec.

Everything after -- belongs to the command, flags included. Without --, leftover positionals are still accepted, so husk exec box ls works — but -- is the only form that can carry flags safely.

stdout and stderr stream live, and Husk exits with the command's own exit code.

husk exec docsbox -- uname -sr
husk exec docsbox -- 'echo hi > /work/a.txt; cat /work/a.txt'
husk exec docsbox --json -- ls /work | jq .exitCode

husk shell

husk shell <name|id> [--cwd <path>]

--cwd is parsed but is not in husk help shell.

husk cp

husk cp <src> <dst>

Either side may be name:/path, and exactly one of them must be. Directories are copied recursively.

husk cp ./report.csv docsbox:/work/report.csv
husk cp docsbox:/work/out.json ./out.json
husk cp ./src docsbox:/work/src

A single-letter prefix is a Windows drive, not a computer — no computer name is one character — so husk cp C:\data\x.csv box:/work/ does what a Windows user expects. A // after the colon is a URL scheme, not a path, and is treated as local.

Both sides naming a computer is a usage error with the two-step fix in it.


Agents

husk init

husk init [name] [--out husk.yaml] [--force] [--yes]
FlagEffect
--outWhere to write. Default husk.yaml
--forceOverwrite an existing file
-y, --yesAccept every default without asking

Asks a few questions on a terminal; with --yes, or when piped, it writes defaults.

the file husk init --yes writes

husk run

husk run <husk.yaml|name> [prompt] [flags]
FlagValueDefault
--modelalias or provider/modelfrom the spec
--max-stepspositive integerlimits.maxSteps
--max-costUSDlimits.maxCostUsd
--approveauto | ask | readonlyguardrails.approvalMode
--no-computerbooleanoff
--varKEY=VALUE, repeatable, for {{var}} in the persona
--jsonprint the RunResult instead of streaming

--max-cost and --var are parsed but are not in husk help run.

The prompt is the remaining positionals, or piped stdin:

husk run husk.yaml "summarise /work/notes.md"
echo "why is main red?" | husk run ci-triage

The first positional is resolved as a path first, then as ~/.husk/husks/<name>/husk.yaml. Neither existing is an error naming both:

error no husk named "triage", and no file at /home/me/triage
hint:  run `husk init` to create one, or `husk distill <transcript>` to generate one from a chat

Model text goes to stdout; steps, tool calls and warnings go to stderr, so husk run husk.yaml "…" > answer.txt captures the answer and nothing else.

Ctrl-C stops the run cleanly and leaves the computer alone — nothing is destroyed — and exits 130.

husk validate

husk validate [file] [--json]

Defaults to ./husk.yaml. Prints every problem at once, not just the first. Exits 1 on an invalid file, and --json still emits { valid: false, path, issues }.


Chats

husk import

husk import [path] [--source <s>] [--pick <n>] [--limit <n>]
FlagValueDefault
--sourceclaude-code | chatgpt | cursor | gemini | markdown | universaldetected
--pickchoose by number without being asked
--limithow many candidates to list20
--jsonthe discovered list, or the imported transcript

With no path, Husk searches its own list of locations, lists what it found, and asks you to choose:

husk looked in:
  ~/.claude/projects   Claude Code sessions
  ~/Downloads          ChatGPT and Gemini exports
  ~/.cursor            Cursor chat history
  ./                   markdown and jsonl

Every transcript found in the chosen file is written to ~/.husk/transcripts/<id>.json, unredacted.

husk distill

husk distill <transcript-id|path> [flags]
FlagValueDefault
--modelthe model to distill withauto
--no-modelforce the free heuristic pathoff
--outwhere to writehusk.yaml
--nameoverride the generated husk namefrom the transcript
--forceoverwrite an existing fileoff
--jsonprint { path, confidence, agent, spec }

The heuristic path is free, deterministic and needs no API key. If no model is reachable, husk distill degrades to it rather than failing.


System

husk doctor

husk doctor [--json] [--force]

--force bypasses the 30-second availability-probe cache. It is parsed but is not in husk help doctor.

The first thing to run when something is confusing. Reading the report walks through the output.

husk models

husk models [--all] [--json]

--all includes models from providers with no credentials, marked offline.

husk serve

husk serve [--port 7377] [--host 127.0.0.1]
FlagDefault
--port7377, or HUSK_PORT
--host127.0.0.1, or HUSK_HOST — loopback only unless you change it

Binding a non-loopback host without HUSK_TOKEN is a hard error, because this API can execute shell commands. Self-hosting is the full setup.

husk mcp

husk mcp [--provider <p>] [--session-key <k>] [--keep]
FlagEffect
--providerForce a computer provider
--session-keyReuse one machine across calls
--keepLeave the machine running after the client disconnects

stdout carries JSON-RPC and nothing else; logs go to stderr.

husk version

husk version [--json]

Bare husk version prints one parseable line on stdout — that is what a release script greps for. The node version, platform and state directory go to stderr where they cannot break it.

husk help

husk help [command]

Identical to <command> --help.


Environment variables

VariableRead byEffect
HUSK_HOMEeverythingState directory. Default ~/.husk
HUSK_HOST, HUSK_PORThusk serveBind address and port
HUSK_TOKENhusk serve, @husk-ai/sdkBearer token
HUSK_LOG_LEVEL, HUSK_LOG_JSONeverythingLogger verbosity and format
NO_COLOR, HUSK_NO_COLOR, FORCE_COLORthe CLIColour
HUSK_SESSION@husk-ai/mcpDefault session key
HUSK_REGISTRY, HUSK_IMAGE_TAGcontainer providersOverride the Husk image pair
HUSK_SSH_TARGET, HUSK_SSH_KEYthe ssh providerRemote host and key
FLY_API_TOKEN (or FLY_ACCESS_TOKEN)the fly providerFly credentials
HUSK_FLY_APP (or FLY_APP_NAME), HUSK_FLY_REGION (or FLY_REGION), HUSK_FLY_APIthe fly providerApp, region, API base
TAVILY_API_KEY, BRAVE_API_KEY (or BRAVE_SEARCH_API_KEY)the web bundleEnables web_search; Tavily wins if both are set
CLAUDE_CONFIG_DIRthe Claude Code importerWhere to look for sessions

Model provider keys are listed on Providers.