Every failure in Husk is a HuskError with a code, a message, and a one-line
hint that says what to do next. The CLI prints the message in red and the hint dimmed
underneath; the control plane returns all three as JSON.
error refused: privilege escalation
hint: add a pattern to guardrails.allowCommands in husk.yaml if this is intentional{ "error": { "code": "E_EXEC_DENIED",
"message": "refused: privilege escalation",
"hint": "add a pattern to guardrails.allowCommands in husk.yaml if this is intentional" } }--debug adds the stack and the cause chain. --json puts the same object on stdout
so a script that checks the exit code still gets parseable output on both paths.
The codes
Twenty are declared in @husk-ai/core. The server declares five more.
Two of the twenty are reserved: E_EXEC_TIMEOUT and E_STEP_LIMIT, listed
together in the exported RESERVED_ERROR_CODES. Husk never throws them, because both
conditions produce a result worth keeping — partial exec output, a partly finished run
— and an exception would destroy it. They stay declared so the HTTP mapping has a name
for a gateway 504, and so removing one is a deliberate breaking change. Read
ExecResult.timedOut and RunResult.stopReason instead.
| Code | HTTP | Raised by |
|---|---|---|
E_PROVIDER_UNAVAILABLE | 503 | a computer provider that cannot run |
E_COMPUTER_NOT_FOUND | 404 | a name or id that resolves to nothing |
E_COMPUTER_FAILED | 500 | a machine that would not come up |
E_EXEC_FAILED | 500 | the exec mechanism itself, not the command |
E_EXEC_TIMEOUT | 504 | reserved — never thrown |
E_EXEC_DENIED | 403 | the command or network policy |
E_FS_DENIED | 403 | the path jail, or a failed filesystem operation |
E_QUOTA | 429 | the machine limit, or provider rate limiting |
E_MODEL_UNAVAILABLE | 503 | no provider can serve the model asked for |
E_MODEL_ERROR | 500 | the provider answered badly, or not at all |
E_NO_CREDENTIALS | 401 | a missing or rejected key |
E_SPEC_INVALID | 422 | a husk.yaml, or a malformed request body |
E_IMPORT_FAILED | 500 | transcript import and distillation |
E_TOOL_ERROR | 500 | a tool that could not do what it was asked |
E_BUDGET_EXCEEDED | 429 | the cost ceiling |
E_STEP_LIMIT | 500 | reserved — never thrown |
E_ABORTED | 500 | Ctrl-C, a cancel, or a disconnect |
E_NOT_IMPLEMENTED | 501 | a package that would not load |
E_CONFIG | 500 | configuration that cannot be resolved |
E_INTERNAL | 500 | a bug |
E_HUSK_NOT_FOUND | 404 | server only |
E_RUN_NOT_FOUND | 404 | server only |
E_APPROVAL_NOT_FOUND | 404 | server only |
E_TRANSCRIPT_NOT_FOUND | 404 | server only |
E_ROUTE_NOT_FOUND | 404 | server only |
E_PROVIDER_UNAVAILABLE
You named a computer provider that is not usable, or none is.
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`error no ssh target is configured
hint: export HUSK_SSH_TARGET=user@host, or set labels."husk.ssh" in husk.yamlHusk never silently substitutes a weaker provider for the one you asked for, which is
why this is an error rather than a downgrade. Run husk doctor for every provider's
state and its fix. It is also raised by the model router when a provider exists but
cannot serve the requested model.
E_COMPUTER_NOT_FOUND
error no computer named "nope"
hint: running now: console-demo, docsboxerror "box" matches 3 computers
hint: use the id — `husk ps` shows themAlso raised when a machine's record survives but its backing container, Fly machine or remote directory does not, and when a destroyed machine is used again:
computer cmp_x has been destroyed
computer cmp_x has no container behind ithusk ps --all includes stopped machines. A destroyed one is gone.
E_COMPUTER_FAILED
The machine would not come up. The message carries the provider's own words:
error could not start a docker container: <docker's stderr>
hint: run `docker info` to check the engine is healthyerror could not create the remote workspace: Permission deniedE_EXEC_FAILED
The exec mechanism failed — not the command. A command that exits non-zero is a
successful exec with a non-zero exitCode.
error could not start a shell: spawn /bin/sh ENOENT
error empty command array
error the fly exec endpoint cannot accept stdinE_EXEC_TIMEOUT
Declared in HuskErrorCode and mapped to HTTP 504, and never thrown. A timed-out
exec returns an ExecResult with timedOut: true and the partial output, because the
partial output is usually the useful part. Check result.timedOut, not for this error.
E_EXEC_DENIED
The command deny list, the husk's own denyCommands, or the network policy.
error refused: privilege escalation
hint: add a pattern to guardrails.allowCommands in husk.yaml if this is intentionalerror network policy refuses example.com
hint: add "example.com" to computer.network.allow in husk.yamlerror refused private address 169.254.169.254
hint: loopback, link-local and RFC1918 hosts must be named in computer.network.allowerror refused file: URL
hint: only http and https are fetchable from a huskE_FS_DENIED
The path jail, or a filesystem operation that failed inside the machine.
error path is outside the machine's writable area: /etc/passwd
hint: this computer exposes /work and /tmp; use a path under one of themerror path resolves outside the workspace through a symlink
hint: husk refuses to follow links that leave the machineerror refusing to delete the workspace rootThe jail covers the filesystem API, not shell commands. Details.
E_QUOTA
Too many machines, or a provider rate limiting you.
error already running 8 computers (limit 8)
hint: destroy one with `husk rm <name>`, or raise maxComputers in ~/.husk/config.jsonerror Groq (llama-3.3-70b-versatile) returned 429: rate limit exceeded
hint: Groq is rate limiting. Husk will back off and fall back; set a cheaper model …Only running and creating machines count toward the eight-machine quota. A model
provider's 429 is retried with backoff and then falls back to the next candidate; you
only see the error when every candidate is exhausted.
E_MODEL_UNAVAILABLE
error no model provider is reachable
hint: set ANTHROPIC_API_KEY or OPENAI_API_KEY, or run `ollama serve` for a free local modelerror Unknown model "sonet"
hint: Use provider/model, or one of: opus, sonnet, haiku, gpt, gemini, flash, gemma, llama, qwen, local, free, auto.error no reachable model matches "opus"
hint: try one of: ollama/qwen2.5:7b, ollama/llama3.2:latestAlso raised when a provider answers 404 for a model id it does not serve. Run
husk models for what is actually reachable, and husk models --all to include
providers with no key.
If Ollama is running and you still see "no model provider is reachable", see Troubleshooting.
E_MODEL_ERROR
The provider was reached and did not produce a usable answer: a 5xx, an unparseable stream, a network failure, or every candidate failing.
error Anthropic (claude-sonnet-5) returned 503: upstream connect error
hint: Anthropic is having a bad time. Husk will retry, then fall back to the next provider.error Ollama is unreachable: fetch failed
hint: Check network access to Ollama. Husk will fall back to another provider if one is configured.Retryable. A 400 or 422 is not — the message says so, and the router will not shop a malformed request around six providers.
E_NO_CREDENTIALS
A key is missing, or the provider rejected it.
error Anthropic has no credential configured
hint: Set ANTHROPIC_API_KEY.error Anthropic (claude-sonnet-5) returned 401: invalid x-api-key
hint: ANTHROPIC_API_KEY is missing or rejected. Re-check the value, or run `husk doctor` to see what else is reachable.Over HTTP, also returned when HUSK_TOKEN is set and the request's bearer token is
missing or wrong.
Never retried — a 401 does not improve on a second attempt.
E_SPEC_INVALID
A husk.yaml, or a malformed request body. Every problem is listed at once.
error husk.yaml is not a valid husk
✗ name: name must be lowercase alphanumeric with dashes
✗ limits.maxSteps: Number must be less than or equal to 500
hint: `husk init --force` regenerates a valid file, or see `husk help init`error husk.yaml is not valid YAML: bad indentation of a mapping entry at line 4Over HTTP: missing required query parameter 'path', run requires a non-empty 'input',
body must carry either 'spec' or 'yaml', and a strict-schema rejection of an unknown
key on POST /v1/computers.
details.issues carries the list. POST /v1/husks/validate returns the same list with
HTTP 200 rather than throwing.
E_IMPORT_FAILED
Transcript import and distillation.
error no transcript "abc" — not a stored id and not a fileerror Claude Code import needs a path or content.error Claude Code found no messages
hint: check the file is a conversation export and not an empty or partial oneE_TOOL_ERROR
A tool could not do what it was asked. These reach the model as a tool result rather than ending the run, which is deliberate — the model reads the hint and retries.
Error: /work/src is a directory
Error: oldString and newString are identical, so this edit would do nothing
Error: not a valid URL: htps://example.com
Error: could not fetch example.com: fetch failedE_BUDGET_EXCEEDED
The cost ceiling, checked before the call rather than after.
error anthropic/claude-opus-5 would cost at least $0.0600 for this prompt, over the $0.05 limit
hint: Raise maxCostUsd, shorten the prompt (~12000 tokens), or use a free model: `--model free`.error no auto model can serve a ~12000 token prompt for under $0.05
hint: Raise maxCostUsd, or use `--model ollama/qwen2.5:7b`.Inside a run, the same condition ends the run with stopReason: 'budget' and an error
object carrying this code, rather than throwing — whatever the agent produced is still
in the result. Never retried.
See Cost and budgets.
E_STEP_LIMIT
Declared in HuskErrorCode and never thrown. Hitting limits.maxSteps ends the run
with stopReason: 'step_limit', which is not an error condition — the agent simply ran
out of turns. Check result.stopReason.
E_ABORTED
Ctrl-C, POST /v1/runs/:id/cancel, an HTTP client disconnecting, a stalled SSE
consumer, or a server shutting down.
error request aborted
hint: The caller cancelled this request.Internal messages you may see in a log: run cancelled by the operator,
the caller aborted this run, client disconnected, the consumer stopped reading,
terminal socket closed.
The CLI renders an interrupt as interrupted and exits 130. A run stops between
steps; the computer is left alone.
E_NOT_IMPLEMENTED
A sibling package would not load, or a capability genuinely does not exist for this provider.
error could not load @husk-ai/server: Cannot find module '@husk-ai/server'
hint: run `npm install && npm run build` at the repo rooterror this computer was not created by a provider that can rebuild itIn a monorepo checkout this almost always means a package has not been built. Run
npm install && npm run build at the root.
E_CONFIG
Configuration that cannot be resolved, or a refusal to overwrite something.
error husk.yaml already exists
hint: pass --force to overwrite it, or --out to write somewhere elseerror Model alias "fast" loops back on itself
hint: Fix the modelAliases entry for "fast" in ~/.husk/config.json.error ssh target "user@host:abc" has an invalid port: abcerror refusing to bind 0.0.0.0 without an auth token: this API can execute shell commands
hint: set HUSK_TOKEN=$(openssl rand -hex 32) before `husk serve --host 0.0.0.0`, or bind 127.0.0.1E_INTERNAL
A bug. The hint says so.
error something broke
hint: this is a bug in husk -- please report itAlso used by the SDK for a transport failure and for a malformed SSE frame:
error cannot reach the husk control plane at http://127.0.0.1:7377/v1/computers: fetch failed
hint: start it with `husk serve`, or point the client at the right baseUrlRe-run with --debug for the stack and the cause chain before filing.
Server-only codes
Declared in @husk-ai/server rather than @husk-ai/core, because the API needs a distinct
404 family and core only has E_COMPUTER_NOT_FOUND.
| Code | Message | Hint |
|---|---|---|
E_HUSK_NOT_FOUND | no husk with id <name> | run husk ls to list husks |
E_RUN_NOT_FOUND | no run with id <id> | run husk runs to list recent runs |
E_APPROVAL_NOT_FOUND | no approval with id <id> | approvals expire 120s after they are raised |
E_TRANSCRIPT_NOT_FOUND | no transcript with id <id> | import it again with husk import |
E_ROUTE_NOT_FOUND | no route for GET /v1/x | see docs/API.md for the endpoints this server serves |
Handling them
import { HuskError, isHuskError } from '@husk-ai/core';
try {
await manager.create({ flavor: 'python' });
} catch (err) {
if (isHuskError(err) && err.code === 'E_QUOTA') {
console.error(err.message, err.hint, err.details);
}
throw err;
}err.toJSON() gives { name, code, message, hint, details } — the same shape the
control plane puts on the wire.