Husk
GitHub

Reference

husk.yaml schema index

Every field of HuskSpec as a flat lookup — path, type, exact zod constraint, default — followed by one fully annotated file.

This is the lookup table: every leaf path in HuskSpecSchema, with the constraint the validator actually enforces. For the prose walkthrough — what each block is for, with worked examples — read husk.yaml instead.

name is the only required field. Everything else has a default, so the smallest valid file is one line:

name: my-bot

Top level

PathTypeConstraintDefault
apiVersionliteral"husk/v1"husk/v1
namestring1–64 chars, ^[a-z0-9][a-z0-9-]*$required
displayNamestring≤ 96 chars
versionstring0.1.0
descriptionstring≤ 280 chars""
modelstringauto
fallbackModelsstring[][]
temperaturenumber0–2
personastring""
knowledgeobject[]see below[]
examplesobject[]see below[]
toolsstring[]["computer"]
computerobjectsee belowall defaults
limitsobjectsee belowall defaults
guardrailsobjectsee belowall defaults
memoryobjectsee belowall defaults
triggersarraydiscriminated on type[{ type: cli }]
metadatarecordany values{}
originobjectsee below

knowledge[]

PathTypeConstraintDefault
knowledge[].titlestringrequired
knowledge[].contentstringrequired
knowledge[].sourcestring

No length cap on either required field. At run time each item is appended to the system prompt under ## Reference as a ### <title> block.

examples[]

PathTypeConstraintDefault
examples[].userstringrequired
examples[].assistantstringrequired

origin

Provenance, written by husk distill. Optional as a whole; source is required once the block exists.

PathTypeDefault
origin.sourcestringrequired
origin.transcriptIdstring
origin.importedAtstring
origin.messageCountnumber

computer

PathTypeConstraintDefault
computer.enabledbooleantrue
computer.providerstringnot validated against the provider listbest available
computer.flavorenumbase | python | node | fullbase
computer.imagestringoverrides flavor
computer.cpusnumber> 0, ≤ 64provider default (2)
computer.memoryMbinteger> 0, ≤ 131072provider default (2048)
computer.diskMbinteger> 0provider default (2048)
computer.idleTimeoutSecinteger≥ 0; 0 disables900
computer.maxLifetimeSecinteger≥ 0; 0 disables0
computer.packagesstring[][]
computer.setupstring
computer.envrecord of string{}
computer.workdirstring/work
computer.persistbooleanfalse
computer.network.modeenumnone | egress | fullegress
computer.network.allowstring[]one leading *. wildcard supported
computer.network.denystring[]applied even in full
computer.mounts[].sourcestringhost pathrequired
computer.mounts[].targetstringpath in the machinerequired
computer.mounts[].readonlybooleantrue

limits

PathTypeConstraintDefault
limits.maxStepsinteger1–50024
limits.maxCostUsdnumber≥ 00.5
limits.maxTokensinteger> 0200000
limits.timeoutSecinteger> 0300
limits.execTimeoutSecinteger> 0120
limits.maxOutputBytesinteger> 0262144

Every ceiling is checked before the next model call, not after. See Cost and budgets.

guardrails

PathTypeConstraintDefault
guardrails.approvalModeenumauto | ask | readonlyauto
guardrails.denyCommandsstring[]regex, falls back to substring[]
guardrails.allowCommandsstring[]regex, falls back to substring[]
guardrails.refusestring[]free text[]
guardrails.redactSecretsbooleantrue

allowCommands is checked first and beats both denyCommands and the built-in deny list. See Isolation and Approvals.

memory

PathTypeConstraintDefault
memory.enabledbooleanfalse
memory.backendenumsqlite | memory | filesqlite
memory.windowTurnsinteger> 020
memory.summarisebooleantrue

triggers[]

A discriminated union on type. Seven variants; three are mounted by the server.

typeFieldTypeDefault
httppathstring/
authnone | tokentoken
cronschedulestringrequired
promptstringrequired
webhookpathstringrequired
secretstring
discordchannelsstring[][]
mentionOnlybooleantrue
slackchannelsstring[][]
mentionOnlybooleantrue
telegramallowliststring[][]
cli

discord, slack and telegram parse and validate and are never mounted. See Triggers. http's auth field is stored and never checked.

tools[]

Bundle names expand; individual tool names are also accepted.

BundleExpands to
computershell, expose_port, computer_info
filesread_file, write_file, edit_file, list_dir, search_files, move, delete
webfetch_url, plus web_search when TAVILY_API_KEY or BRAVE_API_KEY is set
httphttp_request

A tool that cannot work is skipped rather than registered-and-broken. Tools needing a machine are dropped when computer.enabled is false, and http_request is opt-in — naming the http bundle counts as naming it. An unknown tool name is dropped silently (at debug log level).

One annotated file

husk.yaml
# The schema version. Bumped only on a breaking change.
apiVersion: husk/v1
 
# Required. Lowercase alphanumerics and dashes, 1-64 chars. This is the URL
# segment, the CLI handle, and the computer's name.
name: ci-triage
displayName: CI Triage        # <= 96 chars, human label
version: 0.2.0                # yours, not husk's
description: Reads failing CI logs and says what broke, in one paragraph.
 
# An alias or a provider/model id. `auto`, `free` and `local` are strategies.
model: sonnet
# Only honoured on the control-plane path; `husk run` pins one model.
fallbackModels: [flash, gemma]
temperature: 0.2              # omit to use the provider's default
 
# The system prompt. {{name}} placeholders come from `husk run --var name=…`.
persona: |
  You triage CI failures for a TypeScript monorepo.
 
  Always read the failing job's log before forming an opinion. Quote the first
  line that actually failed -- not the last line, which is usually the runner
  giving up.
 
  Answer in one paragraph. No preamble.
 
# Appended to the prompt under "## Reference".
knowledge:
  - title: Repository layout
    content: |
      packages/* are libraries, apps/* are deployables. Any change under
      packages/core forces a full rebuild.
    source: https://github.com/org/repo/blob/main/README.md
 
# Few-shot pairs. Both sides required.
examples:
  - user: main is red
    assistant: The `build (node 20)` job failed at `packages/core` with TS2345.
 
# Bundles, individual tool names, or both.
tools: [computer, files]
 
computer:
  enabled: true               # false removes the machine and every tool needing one
  flavor: node                # base | python | node | full
  # image: ghcr.io/me/custom:1   # overrides flavor
  cpus: 2
  memoryMb: 2048
  diskMb: 2048
  idleTimeoutSec: 600         # destroy after 10 min with no exec; 0 disables
  maxLifetimeSec: 0           # hard ceiling regardless of activity; 0 disables
  workdir: /work
  persist: false              # true keeps the filesystem across restarts
  packages: [pnpm]            # installed on first boot by the flavor's manager
  setup: mkdir -p /work/out   # one shell snippet, once, after creation
  env:
    GITHUB_TOKEN: ${GITHUB_TOKEN}   # explicit values survive the env scrub
  mounts:
    - source: ./fixtures
      target: /work/fixtures
      readonly: true          # the default; set false deliberately or not at all
  network:
    mode: egress              # none | egress | full
    allow: ['*.github.com', 'objects.githubusercontent.com']
    deny: ['telemetry.example.com']   # refused even in `full`
 
limits:
  maxSteps: 20                # 1-500
  maxCostUsd: 0.25            # 0 means "local models only"
  maxTokens: 200000
  timeoutSec: 240             # wall clock for the whole run
  execTimeoutSec: 120         # per exec inside the machine
  maxOutputBytes: 262144      # per tool result, before redaction
 
guardrails:
  approvalMode: auto          # auto | ask | readonly; `ask` fails closed
  denyCommands:
    - 'git\s+(push|commit|reset\s+--hard)'
  allowCommands: []           # beats denyCommands AND the built-in list
  refuse:
    - deploying, reverting, or merging anything
  redactSecrets: true
 
memory:
  enabled: false              # the only gate; off means history is never trimmed
  windowTurns: 20
  summarise: true
 
triggers:
  - type: http                # ALL /v1/t/ci-triage
  - type: cron
    schedule: '*/15 * * * *'
    prompt: Any red builds on main in the last 15 minutes? If none, reply "clear".
 
# Free-form, carried through untouched.
metadata:
  owner: platform-team
 
# Written by `husk distill`. `source` is required once the block exists.
origin:
  source: claude-code
  transcriptId: tr_01hxyz
  importedAt: 2026-09-01T10:00:00.000Z
  messageCount: 214

Validation

husk validate                 # ./husk.yaml
husk validate support.yaml
husk validate --json

Every problem at once, each as path: message:

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
  ✗ triggers.0.schedule: Required
hint:  `husk init --force` regenerates a valid file, or see `husk help init`

parseSpec throws E_SPEC_INVALID carrying the same list in details.issues. safeParseSpec returns { ok: false, issues } instead of throwing.

Over HTTP, POST /v1/husks/validate returns { ok, issues? } with HTTP 200 either way — it is a validator, not an operation that can fail.