Skip to content

A language
designed for
agents to write.

Human readability is a nice-to-have. Agent-writability, agent-debuggability, and agent-safety are the goals. This is why Husk exists, and what it chooses.

§ 1 · Motivation

AI agents write a lot of scripts.

Not applications — scripts. The kind of thing you'd reach for bash to do: download these files, transform them, call this API, summarise the result, write it to disk. This is the daily bread of agentic work, and the tooling is bad.

The pain isn't bash specifically. It's the gap between how agents reason about a task and what bash — or Python, or Node — forces them to produce. Every minute and token an agent spends defending against quoting bugs, silent empty-string coercions, or lossy text-stream plumbing is a minute and a token it isn't solving the problem. Worse, those minutes and tokens are frequently spent producing the bugs, not defending against them.

Husk is a language built around that gap. Typed. Planned. Traced. Resumable. One static Rust binary. Not a replacement for your application code — a replacement for the ten-line bash script the agent was going to write and get wrong.

§ 2 · The nine pains

What bash forces the agent to produce.

Every Husk feature answers a concrete failure mode from a real agent session. Here is the list.

  1. Quoting and escaping is a minefield.Every script has a subtle bug waiting where a filename has a space, a quote, a newline, or a $. Agents burn tokens defending against this and still get it wrong.
  2. Errors are silent by default.set -euo pipefail is a ritual performed because the defaults are hostile. A typo in a variable name silently becomes the empty string.
  3. Data is always a string.JSON, CSV, numbers, dates, paths — all just bytes until piped through jq, awk, sed, cut, each with their own dialect. Half the script is format conversion.
  4. No types, no structure.No way to say "this function takes a list of files and returns a map of filename to byte count." Write a comment and hope.
  5. Composition is via text streams.Powerful but lossy. Structured data gets flattened to lines and re-parsed.
  6. Observability is bolt-on.Want to know what the script did? Add echo statements. There is no standard trace.
  7. Idempotency and retries are manual."Download these 50 URLs, skip ones already done, retry transient failures" is forty lines every single time.
  8. Parallelism is awkward.xargs -P exists but composes poorly with error handling. wait and $! are fiddly.
  9. Re-running is scary.Agents can't tell what a script will do before it does it, and half-run scripts leave inconsistent state.

Husk attacks all of these.

Husk's defaults are hostile to every kind of silent failure bash hands an agent. You opt into looseness. You never opt into rigor. — design note
§ 3 · Design principles

Nine commitments.

Principles are the commitments a language makes before it meets a real program. Husk's are specific enough to argue with. If you disagree with one, you probably don't want Husk — and that's fine.

§ 3.1

Structured data is the primitive.

Values have shapes. Text is a specific type, not the universal substrate. Pipelines pass structured records, not bytes. path is a type. json.Value is a type. datetime is a type.

§ 3.2

Strict by default, lenient by annotation.

Undefined variable → error. Failed command → error. Failed pipeline stage → error with which stage. You opt into looseness (try, ?, || default), not out of strictness.

§ 3.3

Everything is a plan before an execution.

Read functions always execute — during both husk plan and husk run. Write functions, marked with @effect, are intercepted in plan mode and replaced with a description of what they would do. The plan reflects reality because reads run against the live filesystem, network, and git state. There is no separate plan code path to drift from the real one.

§ 3.4

Effects are explicit.

Effects are string annotations on functions. @effect("write") declares that a function performs a mutation. Functions without @effect are read-only. The compiler collects effect strings per module; --allow fs.write enforces them at runtime.

# In the fs module
@effect("write")
fn write_text(p: path, data: string) -> Result[Unit, error]

fn read_text(p: path) -> Result[string, error]   # no @effect — always executes

The runtime enforces a two-tier capability system: native operations (fs.*, http.*, git.*) are intercepted and checked; shell escapes and @native calls are declared and auditable.

§ 3.5

Idempotency is first-class.

fs.ensure() converges filesystem state: no-op if already there, atomic write otherwise. fs.ensure(file: "config.json", contents: X) writes via temp-and-rename when needed. The ensure_* convention extends through every domain-specific module — brew.ensure_package, aws.s3.ensure_bucket.

§ 3.6

Structured tracing by default.

Every step emits a structured trace event with inputs, outputs, duration, and effects. JSONL on disk; export to OpenTelemetry with --trace otlp. No echo "got here".

§ 3.7

Resumability via checkpoints.

Checkpoint blocks group related work into units of recovery. If step 14 of 18 fails, husk resume picks up at 14. Completed checkpoints are cached; their return value is persisted as JSON for debuggability.

§ 3.8

No null. No exceptions.

Missing values are Option[T] (Some(x) or None). Fallible operations return Result[T, error] (Ok(x) or Err(e)). The ? operator propagates both. Agents reason about explicit data flow better than propagating exceptions through call stacks.

§ 3.9

Source is truth.

Module metadata, public API, dependencies, and documentation all live in source. No husk.toml, no package.json, no parallel manifest to maintain. husk inspect reads mod.husk and shows the module's identity, API, effects, and dependencies directly. The only generated metadata file is husk.lock.

§ 4 · Non-goals

What Husk is not for.

A language is the sum of what it chooses against, as much as what it chooses for.

  • Not a build system. Use Make, Bazel, or equivalents.
  • Not a config language. Use TOML, YAML, JSON — read them with fs.read_*.
  • Not a workflow orchestrator. Temporal, Airflow, Dagster exist. Husk scripts can call them.
  • Not an application language. If a task needs OOP, a long-running server, or complex state — write Rust or Go.
  • No classes, no inheritance, no metaprogramming beyond decorators.
  • No implicit type coercion. "3" + 4 is an error.
  • No human-first ergonomics that cost agent clarity. Terse, unambiguous, predictable first.

Install Husk. Port one script.
Watch the plan.