Husk
GitHub

Security

Secrets

Two independent controls — nothing credential-shaped enters the machine, and nothing credential-shaped leaves a tool result — plus an honest list of what each one misses.

Two controls, in opposite directions, and neither depends on the other.

  1. Inbound. The environment a command runs with is rebuilt from a small allow-list, so a variable Husk has never heard of is dropped rather than forwarded.
  2. Outbound. Every tool result passes through redact() before it re-enters the conversation, so an agent that cats a .env sees the file and the model sees sk-ant-…[redacted].

The environment scrub

scrubEnv runs before every exec, on every provider. It does not filter your environment — it builds a new one.

Kept, and nothing else:

PATH  HOME  LANG  LC_ALL  TZ  TERM  SHELL  USER  LOGNAME  TMPDIR  PWD
SYSTEMROOT  COMSPEC  WINDIR  PATHEXT  NUMBER_OF_PROCESSORS  PROCESSOR_ARCHITECTURE

Then HUSK=1 is added, and finally anything in computer.env or the exec's own env is written over the top — an explicit value always wins, because you asked for it.

Everything else is dropped: ANTHROPIC_API_KEY, AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN, and also EDITOR, NODE_OPTIONS, npm_config_registry and your shell prompt. A smaller environment is a smaller blast radius, and agents rarely miss it.

Passing a credential deliberately

husk.yaml
computer:
  env:
    GITHUB_TOKEN: ${GITHUB_TOKEN}
husk up scratch --env GITHUB_TOKEN=ghp_...     # for one machine
husk exec scratch --env FOO=bar -- printenv    # for one command

Explicit beats implicit. The decision is now in the file, where a reviewer can see it and a diff can catch it.

Redaction

redact() from @husk-ai/core matches nine formats:

PatternProvider
sk-ant-… (20+ chars)Anthropic
sk-… (20+ alphanumerics)OpenAI and lookalikes
gsk_… (20+)Groq
AIza… (30+)Google
ghp_… (30+)GitHub personal access token
github_pat_… (30+)GitHub fine-grained token
xox[baprs]-… (10+)Slack
AKIA + 16 uppercase alphanumericsAWS access key id
-----BEGIN … PRIVATE KEY----------END …-----PEM private key, any type

A match is masked, not deleted, so the model can still reason about which credential it saw:

sk-ant-api03-AbCdEf...  ->  sk-ant...[redacted]
-----BEGIN RSA PRIVATE KEY-----…  ->  [redacted private key]

The mask keeps the first min(6, floor(length / 4)) characters.

Where it runs

PathRedacted
Tool result content, before it enters the conversationyes
Tool error messages and Denied: … resultsyes, same code path
Router warnings and fallback messagesyes
Provider availability reasons in husk doctoryes
tool_delta events — live stdout as it streamsno
Persisted run events in ~/.husk/runs/<id>/events.ndjsonno
The prompt you typedno
The model's own outputno
Transcripts cached by husk importno

The clamp runs before the redaction, so redaction always operates over bounded input rather than over a 40 MB log.

Turning it off

guardrails:
  redactSecrets: false

The only reason to do this is when redaction is corrupting legitimate output — a build log full of base64 blobs that happen to match sk-. It is on by default and should stay on.

What redaction misses

It is a pattern list. Specifically:

  • Any credential format not in the table. A Stripe key, a database URL with a password in it, a JWT, an SSH private key in OpenSSH's newer format that does not carry the PEM header, a bearer token your own service issues.
  • A secret split across a chunk boundary. Redaction runs on the assembled tool result, so this is rare, but a value spanning the elision marker of a truncated log can survive in halves.
  • Anything the model infers. Redaction is textual. It does not stop a model from noticing that a file exists, or from reading a config value that is not credential-shaped but is still confidential.

Redaction is a seatbelt for the common case. It is not a data-loss-prevention product, and it is not a reason to point an agent at a directory full of production secrets.

The distiller redacts more

husk distill runs a second, wider redactor over the DistilledAgent on its way into husk.yaml — because that file is the artifact people commit to a repository.

CategoryReplaced with
The nine credential formats abovethe same masks
Your home directory, in every spelling~
Email addresses[email]

C:\Users\alice\dev becomes ~\dev; /home/alice/src becomes ~/src. It runs over the name, description, persona, every knowledge title, content and source, both sides of every example, and every distiller note.

What Husk never stores

  • No key is written to ~/.husk. Keys are read from the process environment on every invocation. There is no husk login, no credential file and no keychain integration.
  • No telemetry. Absent from the codebase, not disabled by a flag. The only outbound requests are to the model provider you configured and to a container registry when a flavor's image is not already pulled.

Everything Husk does keep lives under one directory so you can delete it in one move:

~/.husk/
  config.json      settings, model aliases
  .env             local overrides
  husks/           registered husk.yaml files
  computers/       machine metadata and session bindings
  workspaces/      the local provider's filesystems
  runs/            run summaries and events.ndjson  ← unredacted tool output
  transcripts/     imported chats                   ← unredacted
  data/  cache/

Directories are created with mode 0o700.