Engine binary download/verification model has no independent integrity check, and doesn't match its own tagged commit
Summary
The engine binary the launcher downloads and executes on every edit (via the PostToolUse/Stop
hooks) has no independent integrity verification, no re-check after the first download, a
hardcoded upstream-only download URL, and — as shipped at engine-v0.1.5 — doesn't even
correspond to the source commit tagged skill-v4.3.1. This is a materially different, and
materially weaker, trust model than the plain-JS skill releases before the Rust engine
(skill-v4.0.4 and earlier), where the sha-pinned tag covered 100% of the code that actually ran.
This was found while reviewing a re-pin from skill-v4.0.4 to skill-v4.3.1 (commit
cd12f8660e2dde57b9615c8a6b8ea674101f9cfc). Static source review only — no binaries were
downloaded or executed; findings below are read directly from crates/, the shell launcher, and
Cargo.lock at that commit.
Findings
1. The "same-origin sha256 sidecar" is not independent verification.
plugin/skills/impeccable/scripts/impeccable (the shell launcher) downloads, on cache miss,
https://github.com/pbakaus/impeccable/releases/download/engine-v<version>/impeccable-<os>-<arch>
and then fetches <same-url>.sha256 — from the identical release, identical repo, identical
publisher — and compares. Anyone who can push a malicious binary asset to that release
(compromised maintainer account, compromised release CI) can trivially upload a matching
.sha256 right next to it. IMPECCABLE_DOWNLOAD_BASE can redirect the whole fetch (binary +
sidecar together) to an attacker-controlled host, with no separate anchor.
By contrast, this same codebase does have a real independent-trust-root mechanism —
crates/skills/src/bundle_signature.rs — Ed25519 signature verification against public keys
compiled into the binary (include_str! of scripts/bundle-signing-keys.json), used to gate
impeccable install/update/link's download of the skill-content universal.zip. That mechanism
is not applied to the native engine binary — only to markdown/text skill content. The signing
infrastructure already exists; it's just not extended to the artifact that actually executes.
2. No re-verification after the first download.
The launcher's exec order is: IMPECCABLE_BIN env override (no check) → sibling bin/<os>-<arch>/
copy (no check) → ~/.impeccable/bin/impeccable unversioned home binary (handshake-probed only,
not hash-checked) → the version-pinned cache $IMPECCABLE_HOME/bin/<version>/impeccable,
exec'd immediately if present, with zero re-verification → only then does it attempt a fresh
download+sha256-verify. The cache key is the version string (e.g. "0.1.5"), not a content
digest. Once cached, every subsequent hook invocation trusts that file forever with no integrity
recheck — a later compromise of the cached file on disk, or of the release asset between one
machine's first download and another's, is invisible to this mechanism.
3. Download source is hardcoded to this repo, bypassing any downstream fork's own pinning.
Both the shell launcher's default and crates/skills/src/engine_binary.rs's
DEFAULT_DOWNLOAD_BASE hardcode https://github.com/pbakaus/impeccable/releases/download as the
binary source, regardless of which fork or commit a downstream project has actually reviewed and
pinned. Anyone building a "reviewed, controlled provenance" process around a forked/pinned commit
of this repo will find that process never actually covers the artifact that executes — the binary
always comes from this repo's own release infrastructure.
4. The shipped binary doesn't match the tagged source commit it's supposed to represent.
The engine-v0.1.5 tag (what the shipped VERSION file requests) peels to one commit
("Release engine 0.1.5, CLI 4.1.0 and skill 4.3.0"), which is earlier than the commit tagged
skill-v4.3.1 ("Release skill 4.3.1"). Diffing them shows real crates/ changes landed after the
engine release was cut (e.g. crates/context/src/hook_markers.rs,
crates/skills/src/hook_manifest.rs, plus tests) that are not in the binary every machine
will actually download and run, even though skill-v4.3.1 is the tag someone auditing the source
would review. Reviewing the crates/ tree at a given skill tag does not fully describe the
executing artifact.
5. Hooks run unsandboxed, and the blast radius is broader than one interactive session.
plugin/hooks/hooks.json wires PostToolUse (Edit|Write) and Stop to exec the downloaded engine
binary directly. Tracing crates/cli/src/main.rs's dispatch, the hook/hook-before-edit
subcommands route only into local detector code in the reviewed commit (no network primitive
found there) — but the same compiled binary already links ureq+rustls and reads
OPENAI_API_KEY from the environment elsewhere in the same binary (generate_image.rs,
context_cli.rs's update check). A compromised binary would need no new capability to exfiltrate
— the network stack and env-reading code already ship in the artifact every hook invocation execs.
plugin/skills/impeccable/reference/hooks.md documents this hook firing across multiple AI coding
tools, including one whose cloud-agent mode reads a team-committed hook config file directly
from a repo's default branch — meaning this isn't necessarily confined to one developer's local
secrets; a repo that commits that hook file runs the binary inside a cloud-agent context too, with
no sandboxing described or found anywhere in the launcher or engine source.
Prior related issue (#479) — and why it doesn't cover this
#479 ("Verify skill bundle integrity before extraction") raised the same class of concern —
unverified downloaded content extracted and later executed — against the pre-engine JS
architecture's downloadFile+extractZip path for the skill bundle. That issue predates the Rust
engine binary (reviewed "at v3.5.0") and reads as the likely origin of the
bundle_signature.rs Ed25519 mechanism that now protects the skill-content zip.
That fix, however, only covers the skill-content bundle (markdown/text universal.zip). It was
never extended to the native engine binary introduced by the engine-v* release channel — the
compiled artifact hooks actually exec on every edit, which is what findings 1–5 above are about.
The two download paths (skill content vs. engine binary) now have genuinely different trust
models: one has an independent signature anchor, the other has a same-origin checksum with none of
the properties #479 asked for (no independent signing key, no re-verification, source pinned to
one hardcoded host).
Suggested remediation
- Extend the existing Ed25519 bundle-signing mechanism (or an equivalent, e.g. minisign/cosign) to cover the engine binary release artifacts, with the public key(s) compiled into the CLI/launcher rather than fetched alongside the binary.
- Re-verify the cached binary's signature/hash on some cadence (or at minimum, allow/encourage a content-addressed cache key instead of a bare version string) rather than trusting an already-cached file forever.
- Let
IMPECCABLE_DOWNLOAD_BASE(or an equivalent override) be paired with the signing-key check so a downstream fork's pinned binary can be verified independently of which host served it. - Ensure the tagged release commit (
skill-vX.Y.Z) and the binary release tag (engine-vA.B.C) it depends on stay in sync, or document explicitly which commit the shipped binary actually corresponds to at each skill release.
Happy to provide the specific commit SHAs referenced above if useful, or to open follow-up issues per finding if that's preferred over one combined report.
Source: pbakaus/impeccable