Skip to content

Security: Nibir1/Helix

Security

docs/SECURITY.md

Security Posture

Helix is designed with a "trust but verify" model. The LLM output is treated as untrusted input. Every shell command, git action, and package installation passes through strict structural validators before reaching the OS. Dangerous operations require explicit typed confirmations (e.g., "YES, FORCE PUSH").

Security Policy

Authorized Use Only

Helix is designed as a defensive cybersecurity platform and an AI-powered productivity shell. While it includes reconnaissance engines (/scan) and exploit references (via Exploit-DB integration), these are strictly for:

  • Authorized penetration testing and red team operations.
  • Defensive threat intelligence and detection engineering.
  • Educational purposes in controlled environments.

Unauthorized scanning, exploitation, or malicious use of Helix is strictly prohibited.

Safety Guards

Helix treats AI-generated output as untrusted. To prevent catastrophic accidents, the following safety layers are enforced:

1. Shell Safety Pipeline

  • Hard Blocks: Destructive patterns like rm -rf /, mkfs, curl | sh, eval, and redirection onto a raw block device (> /dev/sda, >> /dev/nvme0n1, and the hd*, vd*, disk*, rdisk* families) are blocked at the parser level. Reading a device and writing to /dev/null are untouched.
  • Risk Tiering: Medium-risk commands (e.g., sed -i, chmod, chown, and any redirection that writes) require explicit user confirmation. Spacing does not change the tier: echo x > f, echo x >f and cat a 2>err.log are all Medium. A descriptor duplication (2>&1) writes no file and stays Low.
  • Directory Sandbox: Write and delete operations are confined to the current working directory and its subdirectories. Absolute paths outside the sandbox are rejected.

Both of the guarantees above were false until 2026-09-08, and are recorded here rather than quietly corrected. The hard-block rule against device writes was written >:\\s*/dev/sd[a-z], where a Go raw string makes \\s a literal backslash and an s — so it demanded the text >:\ and matched nothing a shell produces. cat /dev/zero > /dev/sda passed it. Separately, the risk tier for redirection tested for " > " with spaces on both sides, so echo x >/dev/sda was classified Low — the tier that does not ask — and echo key >~/.ssh/authorized_keys with it. Both rules are now asserted by behaviour rather than by pattern text, including a test that the old expression really was inert, so a future rewrite cannot make them ornamental again.

2. Git & Package Safeguards

  • Typed Confirmations: Dangerous Git operations (push --force, reset --hard, clean -fdx) require the user to type an exact confirmation phrase.
  • Critical Package Protection: Helix blocks the removal of critical system packages (e.g., libc6, systemd, bash) to prevent OS corruption.

3. Reconnaissance Authorization

  • The /scan engine requires explicit target authorization with a written scope/reason before executing nmap or masscan.
  • Dangerous flags (e.g., masscan --rate 1000000) are blocked to prevent network floods.

4. Instruction Firewall (Prompt-Injection Hardening)

Retrieved knowledge is untrusted data with zero authority. Planner context is built only from sanitized structured fields inside authority="data-only" fences; a per-request canary detects context echo; a fail-closed critic validates any plan that exhibits unsolicited network egress (external URLs absent from the user request); clean local operations execute without extra review, keeping Helix fast for legitimate red-team work. See docs/threat_model.md.

5. Kernel-Grade Confinement

In strict mode (/sandbox strict), write and delete operations outside the jail root are denied by the OS kernel (Seatbelt on macOS; bubblewrap or Landlock on Linux). This closes the gap left by advisory path validation: even a confused or injected child process cannot write outside the sandbox. Where no kernel backend exists, Helix warns and degrades to advisory confinement.

5a. Confined Archive Extraction

Helix extracts exactly one third-party archive: the standalone piper speech binary, fetched over the network and then executed. Extraction writes through an os.Root opened on the destination, so every path is resolved inside that directory by the kernel. Entry names are additionally required to be local (filepath.IsLocal, after slash conversion), which rejects absolute paths, .. traversal, and Windows reserved device names.

The os.Root is the load-bearing half, not the name check. A name-based guard can only judge the path an archive declares; it cannot know what the filesystem will do with it. ~/.helix/piper is reused across installs and upgrades, so if anything in that tree is a symlink pointing out of it, an entry named piper/lib/x is perfectly local by every string test and a checked filepath.Join still follows the link straight out. A regression test extracts exactly that archive into exactly that tree and asserts the file outside is untouched.

The self-updater's extraction is safe by a different route: it never uses an entry's path at all, matching the binary by base name and always writing to a filename of its own choosing.

5b. Bounded Path Validation

Sandbox validation resolves symlinks, which costs a chain of lstat calls per path. ValidateCommand therefore bounds the work it will do for one command: results are memoised per path, and the number of distinct paths one command may make it resolve is capped. Passing the cap refuses the command — the sandbox never permits a path it declined to check.

This is a denial-of-service control, not an access-control one. Commands reach the validator from a model, so their length is not under the user's control, and the storage this runs on at the edge is slow. A fuzzer found the original version resolving every absolute-looking word in every command — including read-only ones, which discarded the answer — at a cost that grew without limit alongside the input.

5b2. The file Tool Has No Path Check Of Its Own

The harness gained a file tool (read, list, glob, grep, edit, write). It does not carry its own confinement. Every path goes through the same DirectorySandbox.ValidateSafePath that shell commands get, passed in as a resolver, and the tool package refuses to run at all when no resolver is configured rather than defaulting to "anywhere".

That is deliberate and is the main security decision in the port. The upstream implementation this came from confines paths with its own prefix check against the working directory. Helix already has a stronger one — it resolves symlinks on both sides, folds case for macOS and Windows, handles a target that does not exist yet by validating its parent, and rejects a sibling whose name merely extends the root (/tmp/jail-x against root /tmp/jail). A second, weaker copy of a confinement rule is how a jail grows a door, so there is one copy and the file tool consults it.

Two further properties:

  • Writes are atomic and preserve the existing mode. A temp file in the same directory and one rename. A 0600 file does not become 0644 because something rewrote it, and an interrupted write leaves the original intact.
  • Content a file tool returns is data, never instruction. It reaches the planner through the same authority="data-only" execution report as command output. A file in a repository was written by whoever wrote that repository — precisely the provenance the Instruction Firewall exists for — so a read of an attacker-authored source file cannot direct the next plan.

Local policy hooks see file steps too, on the pre-file / post-file events, with the match subject "<action> <path>" so a rule can gate an action, a path, or both. A blocking pre-file hook denies the step, and it runs after the risk tiers have already approved it — hooks subtract permission, never grant it.

5c. What the Setup Wizard May Install

Two install policies exist deliberately, and the boundary between them is the moment of consent.

First boot asks. The system-package stage detects the host's package manager and requests a separate yes for each dependency, showing the exact command first. This is where you decide what Helix may put on your machine.

/blackbox setup does not ask again. Choosing a speech chain is the decision, so what that chain needs — a runtime, a model file, a voice server, or a host package such as Python, git or cargo — is installed and started without a further prompt. Being asked whether you want the file the chain cannot start without has one sensible answer, and declining leaves a configuration Helix has just recommended.

Stated plainly, because it is a real escalation: on Linux those package commands carry sudo, and they run after one selection rather than one selection per package. The controls that remain are the ones that were always doing the work: Helix never runs a guessed package name — a manager with no verified entry in the catalogue produces guidance, not a command — the command is printed before it runs, and installation is confined to the catalogue in internal/deps plus the specific sidecars in the wizard's own table. It will not install a container runtime, and it will not accept a model licence.

The licence gate is walked, not crossed. For CSM's gated weights Helix installs the Hugging Face CLI, opens the model's terms page in a browser and runs huggingface-cli login — and stops there. None of those three is the consent: a pip package is not a decision, showing you a page is not agreeing to it, and a login prompt is not a token. The token you paste is the consent, and it goes to the Hugging Face CLI's own credential store, not to Helix.

Installers inherit stdin, so a command that needs to ask can. sudo on a host without a cached password, and huggingface-cli login waiting for a token, both prompt at the terminal instead of failing with the reason off-screen. This is visible privilege escalation rather than hidden: the command is printed first, and the password goes to sudo, never through Helix.

The one third-party archive Helix downloads is checksum-pinned and extracted under os.Root (§5a). Sources it builds — currently only csm.rs — are cloned from a pinned URL into ~/.helix and compiled there; the build is ordinary cargo, with no privilege beyond the user's.

5d. Configuration Is Replaced, Never Truncated

~/.helix/config.json is written to a temporary file beside it, fsynced, and renamed into place. A rename within one directory is atomic, so the previous file survives every failure before it.

This is not theoretical tidiness. The previous implementation used os.WriteFile, which opens with O_TRUNC — it empties the file and then writes — and the way that fails in practice is a full disk, which is also the moment several other things fail at once and nobody is reading carefully. It was observed one step from destroying a live configuration:

✘ preferences  could not be saved: open ~/.helix/config.json: no space left on device

Losing that file loses the provider, the voice chain and every preference in it. The temporary file is created in the config's own directory rather than TMPDIR, because a rename cannot cross a filesystem and would silently degrade to a copy.

Multi-gigabyte work is refused before it starts. Helix measures free space ahead of the CSM build and the model download and declines with the numbers, rather than compiling for twenty minutes and dying at 95%. A filesystem that cannot be measured is allowed through: guessing that a disk is too small where the syscall failed would block a machine that is probably fine.

6. Telemetry-Free Local Records

Crash reports are written ONLY to local disk (0600), contain redacted environment values (*_KEY, *_TOKEN, *_SECRET, *_PASSWORD), are never transmitted, are capped at 5, and are removable via /purge. The diagnostics package is provably network-free via a CI-enforced import grep test.

The same contract now covers everything else Helix writes about a session: internal/journal (the daemon interaction journal and the opt-in voice transcript log) and internal/metrics (latency and liveness samples) each carry their own CI-enforced grep test proving they import no networking. Code that writes down what you said or did cannot send it anywhere. All of it is 0600 inside a 0700 directory, size-rotated, and wiped by /purge.

/reboot writes one short-lived file under the same contract, minus the rotation it does not need: ~/.helix/reboot.json carries the state a restart needs — mode, working directory, provider and model, and in-progress tasks — plus, for a typed restart only, a 240-character excerpt of the last thing you typed. It is not a second copy of the conversation: duplicating session.json would put everything you said on disk twice, in a file /memory clear does not govern. It is deleted the moment the restarted shell reads it rather than rotated, discarded unread past 12 hours, and wiped by /purge. It stores provider and model names, never a key.

7. Voice Channel Controls

Speech is an untrusted input channel, not a convenient keyboard. Transcribed audio arrives with user authority, so a television, a podcast, or a person in the room becomes text that could otherwise plan and execute. The controls are structural rather than advisory:

  • Risk is capped at Medium. A high-risk command is unreachable from voice whatever the phrasing, and the refusal is spoken as well as printed.

  • Typed confirmations stay typed. Force push, hard reset, worktree clean, deleting a main branch: the voice prompter refuses these outright, so voice cannot satisfy them even with a perfect impersonation. In a full-duplex session Helix goes further and deafens the session for the duration — session.input_audio.mute, verified to stop transcription and turn-taking outright — so nothing in the room can speak the phrase while you read the prompt. If that mute is refused, the confirmation is refused with it.

  • Ending the machine is High risk. shutdown, reboot, halt, poweroff and the macOS osascript spelling are unreachable from voice and blocked at the default posture. They analysed as Low until 2026-09-13 — which under ask means they run with no confirmation — and the Medium voice cap does not reach a Low command either, so neither guard applied. Found when a planner offered to reboot the machine in answer to "reboot yourself".

  • Voice may restart the SHELL, and nothing else in the danger category. /reboot is reachable by voice because it destroys nothing: the continuity record is written before the process ends, so the worst a misheard "reboot" costs is a few seconds, after which the same mode, directory and conversation are back. /purge, /rag-reset, /commit, /config, /hooks, /init, /scan, /setup and /stealth — all nine — remain unreachable, and the distinction is data loss, not severity of sound.

  • A spoken restart MAY install software, by owner decision. /reboot self-updates and does so automatically, from the microphone as well as the keyboard. The reasoning is that the release comes from a repository the owner controls and tags deliberately, so publishing it IS the authorization. The consequence is stated rather than hidden: whoever can publish to the configured repo can replace the binary with no human present, and a misheard "reboot" can trigger that. What still holds is everything in ADR-019 — mandatory checksum, pinned host, build-info proof — plus automatic rollback when the new binary cannot start. update.check: false turns the check off entirely.

  • A spoken restart writes nothing you said. /reboot is voice-reachable, and the continuity record it leaves omits conversation content entirely on the spoken path — so the rule below survives without an exception.

  • Confirmations fail closed. Silence, timeout, or an unintelligible answer counts as "no". A pending confirmation also remembers what it asked about, so a question about stopping listening cannot be answered by a later phrase that means something else.

  • The microphone is open at an idle prompt on a fresh install. This changed on 2026-09-09 by owner decision and is stated here rather than left to be discovered: wake listening (/blackbox wake on|off) defaults to on, so a new install holds the recorder open while the shell sits idle and enters live mode on any sound. What holds it up: nothing is transcribed while it waits (the detector scores chunks and discards them), the state is announced once per session and shown continuously by the standby HUD, it arms only where a recorder, a transcriber and keystroke readiness all exist, turning it on is typed-only while turning it off always works by voice, and one command closes the microphone entirely. Set speech.wake_word.enabled: false in ~/.helix/config.json to decline it — that value is honoured and never overridden by the default. Unix-only; Windows reports unavailable. The residual risk — someone who installs Helix and reads no banner — is recorded as threat V2b in docs/threat_model_voice.md.

  • Once woken, the microphone stays open for the conversation. This is the largest exposure in this document and it changed on 2026-09-11. A wake used to gate every turn; it now opens a conversation in which every utterance is transcribed, sent to an STT provider and planned, with no further gate. Three states, and only one of them closes the microphone:

    microphone ends by
    STANDBY (startup default) open, wake detection only, nothing transcribed any sound → AWAKE
    AWAKE open and transcribing every turn a stop phrase, /blackbox off, or 10 minutes of silence
    MANUAL closed /blackbox wake on — typed only

    What holds AWAKE up: an inactivity stand-down (speech.wake_word.awake_idle_stand_down_s, default 600s) which is the only control that works with nobody present — setting it to 0 removes the bound and should be read as doing exactly that; spoken stop phrases in two strengths, one of which closes the microphone and persists it; a confirmation round for phrases that could plausibly be part of an ordinary sentence; and the rule below, enforced inside the state transition rather than at a command handler, that voice can never reopen a closed microphone. Recorded as threats V2c, V2d and V2e in docs/threat_model_voice.md.

  • Typing during a capture discards the audio. The keyboard is live while Helix listens; a keystroke cancels the capture and the partial clip is thrown away without being transcribed, so audio recorded up to that moment never reaches a provider. There is a test behind that claim rather than a comment.

  • Outside a conversation the microphone opens only for a turn — unless you ask otherwise. Enabling sentence-boundary barge-in (/config barge-in on) lets Helix sample the mic in the pause between its own spoken sentences, so it can be interrupted by voice. That clip follows the same path as every capture — the recorder writes a temp WAV, which is deleted the moment it is read — and then only its loudness is computed. It is never transcribed, never sent to a provider, never logged, and never enters the conversational context. Off by default, scoped to live mode, and shown on the INTERRUPT row of /blackbox status.

  • Spoken input never takes the shell fast path. A confidently classified command line runs as typed when typed; from the microphone it always goes to the planner, because the classifier decides on the first token and ordinary sentences begin with command names.

  • Default-deny command surface. A command is reachable by voice only if the registry marks it so; nine remain unreachable by design (data destruction, scanning, history writes, posture and privacy switches, key entry). One DANGER ZONE command is reachable — /reboot — because it destroys nothing; the criterion is data loss, not how alarming the command sounds.

  • Voice may reduce what is collected, never increase it. "Turn off your eyes" closes the camera and /blackbox log off stops transcript recording, both by voice; opening the camera is an explicit announced act and starting a transcript log must be typed. The rule has no exceptions: /reboot is the case that tested it, and the feature was shaped to fit — a spoken restart stores no conversation content — rather than the rule being amended.

    Two enforcement points were added on 2026-09-11, both places where a spoken word could otherwise have widened something silently:

    • Voice cannot reopen a closed microphone. Moving out of MANUAL is refused for any non-typed cause, and the refusal lives inside the state transition rather than in a command handler — so it holds for every door into that state, not just the one anybody remembered to guard.
    • Spoken export does not persist. Typed at the prompt, export FOO=bar now changes the session. Spoken, it runs in a subshell and says that it did not stick — an exported PATH is silent and lasts the session, which makes it a way to redirect every later command without appearing in any of them.

Full model, including the residual risk accepted for a voice-first assistant: docs/threat_model_voice.md.

7a. Self-Update Trust Model

Helix updates itself through /reboot. What is verified, and what is not, stated plainly because an updater is the highest-consequence code in the project:

  • Verified. The download's SHA-256 against the checksums file published with that release, matched by filename; the URL and every redirect against a pinned set of GitHub hosts; and the payload's own Go build info, proving it is a Helix binary for this machine before it is installed. Any of these failing is a refusal, never a warning.
  • Not verified. The Sigstore signatures the release pipeline produces. Keyless verification with the wrong identity and issuer constraints reports success while proving nothing, under a label that stops anyone looking further — worse than an honest checksum. Helix prints the cosign verify-blob command instead of pretending. See ADR-019.
  • Reversible. The previous binary is kept, and restored automatically if the new one exits non-zero within ten seconds of starting — the failure a checksum cannot catch, which is an authentic release that does not run on this machine.
  • Automatic, by owner decision. Checking is on by default and installing needs no confirmation, on the typed and spoken paths alike. This is the one place Helix trades a prompt for convenience, and it does so because the publisher and the operator are the same person. Turn the whole thing off with update.check: false, or use /reboot now to restart without checking.

8. Camera and Transcript Privacy

  • A restart is the only thing that puts an excerpt of your words on disk without an opt-in, and only when typed. /reboot stores 240 characters of the last typed message in ~/.helix/reboot.json so the resumed shell can say what it was doing; the file is 0600, consumed on read, and expires in 12 hours. The spoken path stores none of it.
  • Camera frames are memory-only, always. One frame at a time, downscaled, held in RAM, never written to disk — enforced by a filesystem-snapshot test. Only metadata (provider, count, timestamp) reaches the journal.
  • Vision is off by default and opens on an explicit, announced act. /blackbox status will not claim the camera is working until a frame has actually arrived.
  • Nothing you say is stored unless you ask. /blackbox log on starts a local transcript log; with it off there is no directory and no file. It records text and metadata only — never audio, because captured clips are deleted the moment they are read.
  • Conversational context retention is opt-in and never reaches disk. A context-conditioned voice (Sesame CSM-1B) needs to hear the last few turns, so enabling speech.tts.context_turns makes Helix hold recent audio in memory for longer than the turn that produced it. That retention is memory only — the store imports no filesystem or networking API, enforced by a test — bounded by both turn count and total bytes, and dropped when live mode ends. The audio was already in memory a moment earlier for transcription; what changes is how long, which is why the bounds are the control. Off by default. It is visible while it is happening: the CONTEXT row of /blackbox status reports how many turns and how much audio are being held, and says retained, unused when turns are being kept that no configured voice can actually consume — retention with a cost and no benefit is the case worth surfacing, not hiding.

Secret Handling

  • API keys are stored in ~/.helix/secrets.json with strict 0600 file permissions.
  • Helix preferentially reads secrets from environment variables to avoid disk persistence in ephemeral environments.
  • No secrets are ever logged, even when HELIX_DEBUG=1 is enabled.
  • Speech keys are namespaced (stt.* / tts.*) in the same store, and a saved key is verified and reused rather than requested again.
  • Misdirected-key guard. A pasted key whose prefix unambiguously belongs to another vendor (sk-ant-, xai-, gsk_) is flagged before it is stored, because GroqCloud and xAI are different companies one letter apart and the mistake otherwise surfaces later as an auth failure on every transcription. The check is negative-only: it never asserts what a valid key looks like, since vendors change formats and a positive rule would start rejecting good keys.

Entering them

A key is read without echo. commands.AskSecret reads the terminal in raw mode and prints nothing back, so the value does not reach the screen, the scrollback or a screenshot.

This was not always true, and the gap was not theoretical. Every key prompt used AskLine — the same reader that asks which provider you want, which echoes by design — so a pasted key was printed in full. It was found when a user sent a screenshot of the Windows setup wizard with a live sk-proj-… key visible in it; the key had to be revoked. Nothing about the bug was Windows-specific. It had echoed on every platform since keys were first asked for, and Windows was simply the first time anyone photographed it.

The prompt itself says what it is about to do with the value. Before anything is typed it names the provider, the page that issues its keys, the file the key lands in with its mode, and the environment variable that avoids the disk entirely:

   API KEY
  ────────────────────────────────────────────────────────────────
  │ PROVIDER  openai
  │ GET ONE   https://platform.openai.com/api-keys
  │ STORED    ~/.helix/secrets.json  (0600, this machine only)
  │ OR SET    $OPENAI_API_KEY  (never written to disk)
  │
  │ Nothing is echoed as you type. The key is never logged, never
  │ passed as a command-line argument, and goes nowhere but openai.

On a terminal that cannot suppress echo the last paragraph is replaced, not softened. It says plainly that the key will be visible on screen and in the scrollback, and suggests pasting it elsewhere. Promising hiding that will not happen is worse than saying nothing, because the reader pastes a credential on the strength of it — which is how a live key ended up in a screenshot.

A provider with no single key-issuing page (custom) gets no URL rather than a plausible one. A wrong link in a security prompt is worse than no link: the reader follows it and then has to work out that Helix, not their memory, was wrong.

Two further properties are worth stating because they are deliberate:

  • Not routed through Prompter. ADR-005 puts /setup on the voice-denied list precisely because it "would have you dictate API keys aloud". A secret that travelled through the same abstraction as an ordinary question would be one refactor away from reaching the voice prompter, so AskSecret reads the terminal directly and there is no channel to misroute.
  • A terminal that cannot hide input says so. Some emulators — MSYS2's MINGW64 among them — hand a Go binary a pipe rather than a console, and echo there belongs to the emulator, not to Helix. Rather than reading silently and looking fixed, the prompt states that input is not hidden, so the choice to paste a key is an informed one.

Removing them

make delete-secrets removes every credential Helix stores and nothing else: secrets.json (all provider keys), daemon.conn.json (the daemon's per-start auth token), and voice_log/ if the opt-in transcript log was ever enabled.

It is deliberately not part of make clean. Cleaning a build must not cost you your API keys — clean used to delete them through a blanket *.json sweep, which is a bug rather than a policy — but revoking what is on a machine is a real thing to want on its own: handing a laptop on, attaching a transcript to a bug report, or after a key has leaked.

Two things it deliberately does not do, and says so when it runs:

  • The Hugging Face token is named, not deleted. It lives in a shared cache that other tools authenticate against, and removing the file leaves a half-revoked login. hf auth logout is the command that actually revokes it.
  • Keys set in the environment are not files and survive it. A target called "delete secrets" invites the opposite assumption, so it prints the check (env | grep -iE 'API_KEY|_TOKEN') rather than letting you infer it.

/purge remains the broader wipe — it reaches models, caches and the stores outside ~/.helix, with sizes, and asks about each group.

Supply-Chain Security & Release Integrity

Every official Helix release is built with a verified supply chain. We generate a Software Bill of Materials (SBOM) in SPDX format and cryptographically sign every release artifact using Sigstore keyless signing.

Verifying a Release

You can verify the integrity and provenance of any downloaded Helix binary or archive using cosign and syft.

1. Install the tools:

2. Verify the signature (Sigstore Keyless):

cosign verify-blob \
  --certificate Helix_Linux_x86_64.tar.gz.pem \
  --signature Helix_Linux_x86_64.tar.gz.sig \
  --certificate-identity-regexp "https://github.com/Nibir1/Helix/.*" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  Helix_Linux_x86_64.tar.gz

3. Inspect the SBOM:

syft helix_Linux_x86_64.tar.gz

Continuous Security Scanning

Every commit and pull request is automatically scanned for known vulnerabilities in Go dependencies using govulncheck and for static application security testing (SAST) using GitHub CodeQL. See .github/workflows/security.yml for details. You can run these checks locally via make sec-scan.

This gate has fired. It held the v1.5.0 release for GO-2026-5942 — a panic parsing a malformed SVCB or HTTPS DNS record in golang.org/x/net, fixed by upgrading to v0.56.0. It is recorded here because a scanner that has never blocked anything is indistinguishable from one that is not running: govulncheck reports by reachability, and this one was reachable from Helix's own code rather than merely present in the module graph — through the full-duplex path, live.Session.negotiate → SetLocalDescription → dnsmessage.Message.Unpack. The same scan reported 25 other advisories in required modules that Helix does not call, and correctly did not fail on them.

One toolchain across all three workflows (Go 1.27). govulncheck reports against the standard library of the Go it runs under, so scanning with an older toolchain than the one that builds the release answers a question nobody asked. CI, the release build and the security scan are pinned to the same version for that reason; go.mod's go 1.25.1 is the language floor the source is written against, which is a different thing. The pin moved off 1.26 because the fuzzing coordinator there could fail a green run at its own deadline (go.dev/issue/75804) — see .github/workflows/ci.yml.

The linter pin moves with it. golangci-lint embeds a type-checker built against a specific Go release and cannot read export data from a newer one, so a Go bump alone fails every job before a line of Helix is checked — with errors naming the standard library inside the toolchain cache rather than any file in this repository. That has happened twice now (v1.59.1 against Go 1.25, v2.5.0 against Go 1.27), so the version is pinned to v2.13.2 — built with go1.27.0 — in .github/workflows/ci.yml, the Makefile's install hint and .golangci.yml's header, and scripts/check-workflows.sh fails if those three ever disagree or if the workflows stop agreeing about Go. A version string copied into three files needs a guard, not a convention.

Reporting a Vulnerability

If you discover a security vulnerability in Helix (e.g., a sandbox escape, a planner injection flaw, or a bypass of the safety pipeline), please report it responsibly.

Do not open a public GitHub issue for security vulnerabilities.

Instead, please email the maintainer or use the GitHub Security Advisories feature to report the issue privately. We will acknowledge receipt within 48 hours and work on a patch.

There aren't any published security advisories