**The action.** `hound hygiene` and `hound supply-chain` only ever saw
repositories somebody had already cloned onto a machine with Hound
installed. action/ runs them on every push and pull request: it
installs the published .deb, verifies it against the same signed
checksum the desktop agent uses, and annotates findings on the lines of
the files they concern so a reviewer sees them in the diff rather than
in a log nobody opens.
report.py will not print a credential it found — GitHub masks only
values registered as secrets, so anything else in an annotation is
readable by everyone who can see the run and stays in the API
afterwards. And it will not report a partial scan as clean: a history
walk that hits its limit says so, because "no findings" and "no
findings in the part we looked at" mean different things to somebody
deciding whether to merge.
**The background.** A radial gradient was set on `html, body` — both,
each 100% tall — so it painted twice and the seam between the two
layers appeared as a band across the middle of the page when scrolled.
It was also a hardcoded near-black the light theme had no way to
override. Three more like it: the active tab, button hover, and the log
panel. The ground is a token now, and every colour in the stylesheet
comes from one, so no rule can put one theme's text on the other's
background.
**The palette.** The light theme now uses houndav.com's values exactly
— #5A58C8 buttons, #147A3D, #9A6100, #C22222 — so the app and the site
are recognisably the same product rather than two guesses at it.
Then measured rather than assumed, and found two failures Joe had not
mentioned: "faint" text was 2.90:1 in dark and 3.37:1 in light, and the
dark button hover was 4.41:1. All three now clear 4.5:1, and all eight
text pairs pass WCAG AA in both themes.
**And four more places still naming ClamAV**, which has not been the
engine for a long time: the auto-update caption said the daemon runs
freshclam, the update log said the same, an error suggested `apt
install clamav`, and a permissions hint pointed at /var/lib/clamav. The
engine swap replaced the code and left the copy describing software
this product no longer runs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Malware scanning asks whether a file is hostile. The check that
actually loses people their accounts is a different one: what has
already been exposed, and what is about to be? crates/hound-supply/src/
hygiene.rs answers it, and runs as part of every project sweep.
- A secret file tracked by git. The emergency case: it is in the
history, in every clone, and in every fork. The advice says rotate
BEFORE `git rm --cached`, because removing a pushed secret does not
un-share it, and a test asserts that ordering.
- Credentials hardcoded in source, recognised by issuer format —
AWS, GitHub, Anthropic, OpenAI, Stripe, Slack, GitLab, npm, PyPI,
Google, and the PEM private-key headers.
- Secret files readable by every account on the machine.
- Secret files with nothing in .gitignore covering them: the near
miss that the next `git add -A` turns into the emergency above.
- GitHub Actions: pull_request_target with a checkout of the pull
request (a stranger's code, your secrets, your write token), a
secret echoed into the build log, a downloaded script piped into a
shell, and third-party actions on a moving tag.
Two rules govern all of it. **Findings are actionable**: no entropy
heuristics, because "high entropy string" is a coin flip a human then
has to adjudicate, and people stop reading after the second false
alarm. Every detector recognises a documented credential format or
reports a structural fact that is true or false. **Nothing secret is
copied into a finding** — a report naming the key it found has moved
the key into a log, a CI artefact, or an assistant's context window,
which is the thing being prevented. There is a test for that.
Tracked-file status comes from parsing .git/index rather than running
git: the sweep is pointed at repositories precisely because they are
not trusted, and starting a subprocess inside one is what a hostile
repository wants.
Against a deliberately bad test repository: four critical, five
warnings, and correctly silent on .env.example and actions/checkout@v4
— flagging those is how a scanner gets ignored.
Also in this release:
- Right-click "Scan for Threats with Hound" in Nemo, Caja and
Dolphin, whose menu entries are system files. GNOME Files and
Thunar keep theirs per-user, so `hound context-menu install`
handles those. The icon is symbolic, so the file manager recolours
it to the menu's own theme instead of dropping a violet dog into a
monochrome menu.
- A right-click while the app is already open hands the request to
the running instance rather than refusing. A menu item that
silently does nothing because the app happens to be open is
indefensible.
- Two fixes for the duplicate tray icon. The updater slept a fixed
600ms after SIGTERM and then started the replacement; if the old
process outlived that, the panel kept its item and the result was
two dogs, the older of which could not be clicked or closed because
nothing was behind it. It now waits for the process to actually
leave /proc, escalating to SIGKILL after five seconds. And the app
itself now holds an advisory lock for its lifetime, so a second
instance cannot exist — the kernel releases the lock however the
process dies, so a stale one is not a state that can happen.
402 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Lockfile parsing for npm (all three lockfile versions), yarn, cargo,
poetry, requirements.txt, go.sum, Gemfile.lock and composer.lock, wired
through the indicator index so a sweep checks real dependencies against
real definitions. Signed packs load in the daemon; the sweep gets the
index; `hound supply-chain` cites the OSV record it matched.
A lockfile is the right thing to read: it names every transitive
dependency at an exact version in one small file, and it lists what WILL
be installed rather than what already is — which matters when the
payload runs during installation.
Every parser is hand-written rather than pulling in a TOML and a YAML
crate. Two fields from each format, and a scanner parsing hostile input
should have as little parsing surface as it can.
The important part of this commit is a false positive it fixes.
Building a pack from the whole crates.io OSV export and sweeping a
project produced TWO criticals: rustdecimal, correctly, and **tokio
1.38.0**, which is not malware and never has been. The export is 1,524
GHSA and 1,206 RUSTSEC vulnerability advisories against 19 malicious-
package records, and the parser treated all of them as malware.
GHSA-2grh-hm3w-w7hv describes a tokio race condition fixed in 1.8.1;
Hound reported a version released years later as malicious.
Two independent bugs, either of which alone is fatal:
* Vulnerability advisories were ingested at all. A malicious package
should not exist; a vulnerable one is a legitimate library with a bug
and most of its versions are fine. Records must now PROVE they are
malicious-package reports — a MAL- id, the malicious-packages-origins
marker, or GHSA's "Malicious code in" wording — and anything
unrecognised is dropped.
* Unrecognised version ranges fell back to "all versions", which is the
opposite of safe. That is what turned a range of 1.8.0-to-1.8.1 into
a verdict on every tokio ever published.
Rebuilt against the same input, the pack now holds 19 indicators rather
than 3,614, rustdecimal is still caught and cites MAL-2022-1 rather than
a GHSA advisory, and tokio and serde are clean. The real tokio advisory
is now a regression fixture, because anything that flags tokio is a
product nobody trusts twice.
Also: definitions loading fails CLOSED on authenticity and OPEN on
everything else. No trusted key means no definitions and a message
saying so, because an operator who believes they are protected and is
not is worse off than one who knows. A pack that fails verification is
skipped and the rest still load. No packs at all is a working daemon —
install scripts, prompt injection, pickles and MCP audits need no feed.
There is deliberately no placeholder signing key compiled in. A fake key
that looks real is how a development shortcut becomes a shipped
vulnerability; an empty trust store is noisy in the way that gets fixed
before release. HOUNDD_DEFS_KEY supplies one for development.
294 tests pass across the workspace.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 4's detection core, as a standalone crate. This is the part with no
competitor on Linux, and deliberately the part with no Linux in it —
file parsing and logic only, no fanotify, no /proc, no eBPF — so the
macOS and Windows port is weeks rather than a second product.
Five detectors, 88 tests:
pickle GLOBAL/STACK_GLOBAL walk over .pt/.ckpt/.pkl/.joblib.
torch.load runs a stack machine; a model file is a
program and downloading weights is a code-execution
decision.
injection Instructions aimed at a coding agent in CLAUDE.md,
AGENTS.md, .cursorrules, copilot-instructions.
installscript preinstall/postinstall hooks that curl|sh, decode and
run, reach for credentials, or install persistence.
typosquat Damerau-Levenshtein against popular names, plus
slopsquat detection: new + near-zero downloads + one
edit from something popular is the signature of a name
a model invented and somebody then registered.
mcp Servers fetched unpinned at launch, handed secrets, or
pointed at $HOME or credential paths.
Wired through `supply.sweep` on the socket and `hound supply-chain
<path>`, which exits 1 on a critical so it drops into CI.
Three things worth recording:
* Scoring is by independent category, not by keyword count. One
suspicious phrase is a phrase; two categories at once is an attack.
A file that only says "ignore previous instructions about formatting"
is a warning, not a critical.
* Proximity matters more than presence. The first version flagged an
entirely ordinary conventions file, because it mentioned ".env" in
one paragraph and "prefer small commits" in another. A credential and
a movement verb now have to appear within a sentence of each other.
The test that caught it is kept as the regression.
* Pickle call detection has to come out of the opcode walk, not a byte
search. REDUCE, INST and OBJ are the ASCII letters R, i and o, which
also occur inside every string the stream carries — searching raw
bytes finds the o in "os" and reports a call that never happens,
turning every warning into a critical.
Every finding carries a plain-language explanation and a next step, and
there is a test asserting explanations do not leak rule identifiers or
jargon. The audience includes people who cannot triage a YARA match and
should never be shown one.
Verified against a demo project holding a squatted @vue plugin with a
curl|sh postinstall, a poisoned CLAUDE.md in a vendored repo, an
unpinned MCP server holding a GitHub token, and a pickle calling
os.system — four criticals and one warning, while the legitimate
CLAUDE.md, the real express manifest and the properly-scoped MCP server
beside them stayed clean.
189 tests pass across the workspace.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>