Four tools over MCP stdio, so a coding assistant can ask Hound about a
project at the moment it matters rather than after:
hound_check_project the full supply-chain sweep
hound_check_package is this package known bad, before installing it
hound_check_file one file: a model, a lockfile, a manifest
hound_check_mcp_config audit the servers your assistant already trusts
READ-ONLY, permanently. There is no hound_quarantine, no hound_delete,
no way to change a setting. An agent can be persuaded by the very
repository it is inspecting — that is the threat this product exists to
detect — so a destructive MCP tool would hand the attacker exactly the
capability they were reaching for. A test asserts every tool name starts
with hound_check_ and contains none of quarantine/delete/remove/restore/
settings/set/update/install/write/exec, so the property cannot erode.
The output is written to be read twice: by the model that called the
tool, and by the human reading that model's summary. That rules out rule
identifiers and jargon — a test greps the output for both — and it rules
out ambiguity about severity. A model reading "1 warning" may well decide
to proceed, so a critical finding leads with an explicit recommendation
and names the consequence: this runs code during installation, before
any of the project's own code runs.
Clean results say what they did NOT check. "Nothing wrong" that reads as
a blanket endorsement is worse than no answer, so a clean project notes
it is not a source review, a clean name check notes it did not read the
package's contents, and a clean MCP audit still points out that every
server listed runs with your permissions and is called without asking.
The server passes its own audit, which was the point:
{ "mcpServers": { "hound": { "command": "/usr/bin/hound-mcp" } } }
No npx, so nothing is fetched at launch. No env, so no secret is handed
over. No path argument, so it is granted no directory. Hound's MCP audit
flags all three in other people's configs; a security tool that failed
its own check would have answered the only question that mattered.
Verified against the installed binary.
Protocol notes, since both are easy to get wrong and fatal:
- a notification carries no id and must never be answered; MCP sends
notifications/initialized straight after the handshake, so replying
corrupts the stream on the first exchange
- an id of 0 is still an id, and treating it as absent silently drops
the first call from any client that counts from zero
- a tool failure is a RESULT with isError, not a JSON-RPC error: the
agent should see "I could not read that path" as an answer it can act
on, not a transport fault that looks like a broken server
- nothing but protocol messages ever goes to stdout; diagnostics go to
stderr, because one stray println corrupts the session
Shipped in the .deb, and the postinstall now prints the config entry.
334 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|---|---|---|
| assets/icons | ||
| crates | ||
| dist | ||
| gui | ||
| packaging | ||
| .editorconfig | ||
| .env.example | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
| rust-toolchain.toml | ||
Hound Antivirus
A premium, freemium antivirus for Linux. One engine, three faces:
a Rust daemon (houndd), a CLI (hound), and a Tauri GUI with a
system-tray sentinel that changes color with your security state.
Built for the distros people actually run: Ubuntu, Debian, Linux Mint, and anything else that ships ClamAV.
Repository layout
antivirus/
├── Cargo.toml # Rust workspace
├── crates/
│ ├── hound-api/ # shared wire types + socket client (daemon/CLI/GUI all use it)
│ ├── houndd/ # the daemon: Unix-socket API over a pluggable engine
│ └── hound/ # CLI client
├── assets/icons/ # dog-head brand mark + 4-state tray ladder
└── gui/ # Tauri 2 desktop app (system tray + scan UI)
Architecture
houndd (Rust daemon — the engine)
┌──────────────────────────────────┐
│ ScanEngine trait │
│ ├─ L1 ClamAV signatures (now) │
│ ├─ L2 Curated threat packs(Pro)│
│ ├─ L3 Behavioral monitor (Pro) │
│ └─ L4 Supply-chain checks(Pro) │
└──────────────┬───────────────────┘
Unix socket (JSON-RPC, line-delimited)
┌───────────┼───────────┐
hound CLI GUI (Tauri) future modules
The daemon is the only process that touches a scanning engine. CLI and GUI are thin clients — so future suite tools (firewall, updater, …) plug into the same socket.
Swapping the engine (the ClamAV seam)
ClamAV is a temporary dependency. Everything ClamAV-specific — version
probe, signature freshness, the clamscan subprocess + output parsing,
freshclam — lives in one file behind a four-method trait:
crates/houndd/src/engine.rs
trait ScanEngine { name; probe; scan; update }
struct ClamAvEngine // today
const ENGINE: ClamAvEngine // ← flip this line when the native
// engine lands; nothing else in the
// daemon, CLI, GUI, or wire API moves
The wire stays engine-agnostic: Status.engine names the implementation
("clamav" today) and Status.db carries what any signature store has —
a file name and a timestamp. When our own Rust engine ships, it's a new
ScanEngine implementation, a one-const flip, and the tray/CLI/GUI simply
start reporting the new engine name.
Icon system
The brand mark is a solid dog head (assets/icons/hound.svg), a single
flat fill. It ships in two treatments:
- Brand ladder
hound-{16,22,24,32,48,256}.png— native periwinkle#9896E0, for the window icon, About box, and marketing. - Tray-state ladder
state-<name>-{16,22,24,32,48}.png— the same path re-tinted per security state, for the system-tray sentinel:
| State | Fill | Meaning |
|---|---|---|
protected |
#22C55E green |
up-to-date / protected / clean |
scanning |
#F59E0B amber |
scan in progress / signatures need update |
threat |
#EF4444 red |
infection found |
paused |
#6B7280 gray |
real-time monitor off |
Green is the good state; amber is work in progress, never a failure.
Quickstart (development)
Prereqs: Rust (see rust-toolchain.toml), Node 20+, ClamAV, and the
Tauri system libs (libwebkit2gtk-4.1-dev, libgtk-3-dev, libayatana-appindicator3-dev).
# 1. Signatures (needs the clamav freshclam DB)
sudo freshclam
# 2. Daemon (terminal 1)
cargo run -p houndd
# 3. Scan (terminal 2)
cargo run -p hound -- status
cargo run -p hound -- scan ~/Downloads
# 4. GUI
cd gui && npm install && npm run tauri dev
Verifying the engine with the EICAR test file
EICAR is the industry-standard 68-byte test signature — every AV that works will flag it. Generate it and scan it:
printf 'X5O!P%%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > /tmp/eicar.com
cargo run -p hound -- scan /tmp/eicar.com
# expect: exit code 1, "Eicar-Test-Signature FOUND"
Updating signatures
hound update wraps freshclam (trying sudo freshclam first, since
plain-user runs can't write /var/lib/clamav and /var/log/clamav). The
GUI's "Update Signatures" button drives the same RPC and shows the log.
cargo run -p hound -- update # or: hound update --json
The GUI (gui/)
A Tauri 2 desktop app — a thin view over the same houndd socket the
CLI uses (via the shared hound-api client), so the window and the
command line never disagree about your security state.
gui/
├── dist/ # the front-end (vanilla HTML/CSS/JS, premium dark shell)
└── src-tauri/ # Tauri 2 shell + system-tray sentinel
The tray sentinel swaps the 4-state icons (green/amber/red/gray) as your state changes; the window shows a live protection hero, a scan progress bar, a results table, and the signature-update log.
Build it:
cd gui
npm install
npm run tauri dev # dev with hot reload
npm run tauri build # → .deb in src-tauri/target/release/bundle/
Git conventions
-
Branch
mainis deployable; small, focused commits. -
No hardcoded secrets. For pushes, the bot token lives in a repo-local credential file (never tracked):
git config credential.helper 'store --file=.git/.git-credentials' chmod 600 .git/.git-credentials echo 'https://<user>:<token>@git.joelovestech.com' >> .git/.git-credentials -
Commits: imperative subject, ≤ 72 chars. Example:
houndd: add line-delimited JSON-RPC socket API
License
TBD — core daemon likely proprietary (freemium), shared CLI possibly OSS.
Decision pending; workspace.package.license = MIT is a placeholder.