PushWarden command-line reference
PushWarden is a single binary. Every feature is a subcommand:
pushwarden <command> [options]
This page documents each command, its options, exit codes, the configuration keys, the files PushWarden keeps, and the environment variables it reads. For the install one-liner and a description of the threat, see the website and the documentation site.
Contents
- Conventions
- Quick start
- Commands
- Configuration keys
- Files and directories
- Environment variables
- Severities, actions and threat names
- Recipes
- Troubleshooting
Conventions
- Flags accept one or two dashes:
-fixand--fixare the same. Flags may come before or after positional arguments. pushwarden [dirs]with no command is short forpushwarden scan [dirs].pushwarden <command> -hprints that command’s options.pushwarden help <command>prints this documentation for it, in the terminal, offline.- Exit codes follow one rule everywhere:
0clean or done,1threats found or not fully fixed,2usage error, nothing scanned, or an internal failure. Commands that only display information return0. - Nothing needs root except
protect, which edits the firewall and the hosts file. - Every destructive step is reversible. Files are copied to the quarantine before they are
stripped or deleted, and
pushwarden historyputs them back.
Quick start
pushwarden install # background guard + editor hardening + first scan
pushwarden status # is everything running?
pushwarden scan --home # audit every project under your home folder now
pushwarden github-clean # dry run: which of my GitHub repos and branches are infected?
pushwarden github-clean --apply # fix and push them
pushwarden ui # clean your GitHub repositories the easy way, on guided screens
Commands
scan
One-off scan of repositories and of this computer.
pushwarden scan [options] [directories]
With no directories it scans the current working directory. In a terminal a progress bar shows
which repository is being scanned; in CI or a pipe, numbered progress lines are printed instead. Each directory is walked for git
repositories and for projects with a package.json, go.mod or composer.json. Every script,
config, font, image and .vscode/*.json file in them is checked. Then the host is checked:
running processes, network connections to known command-and-control (C2) addresses, persistence
(systemd units, LaunchAgents, scheduled tasks, cron, shell rc files), remote access tool
footprints, editor injection and exposed credentials.
| Option | Effect |
|---|---|
--home |
also scan the common project directories under your home folder (or scan_roots from the config) |
--deep |
also descend into node_modules and vendor (slow) |
--history |
also read every file version ever committed, not just recent changes. Finds a payload that a later commit hid but that a fresh clone still downloads. Slower; it reports and prints the git filter-repo command, and never rewrites history for you |
--configs-only |
only check the known config and entry-file names, the old v4 scope |
--no-system |
skip the host checks |
--no-repos |
skip the repository checks |
--verbose |
list every repository, including clean ones |
--fix |
act on CRITICAL findings without asking: strip the payload or quarantine the file, remove PolinRider entries from .gitignore, kill malicious processes, remove persistence. Reversible. Stripping a payload and removing .gitignore entries need Pro; the free version quarantines those files whole |
--dry-run |
show what --fix would do and change nothing |
--gui |
ask about each malicious file with a native dialog instead of in the terminal |
--no-prompt |
report only. Never ask, never change a file |
--notify |
send a desktop notification and, if configured, a webhook when anything HIGH or above is found |
--json FILE |
also write the report as JSON to FILE |
--no-report |
do not save the report under the data directory |
--ci |
CI mode: no colour, compact output, never prompt |
--js-all |
accepted for compatibility; scanning every script is already the default |
What happens when something is found. In an interactive terminal, a CRITICAL finding with
strong evidence triggers a question per file: remove, or keep and allow. With --gui the question
is a native dialog. With --fix the reversible action is taken without asking. With --no-prompt
or --ci nothing is touched. Findings that PushWarden cannot fix on its own (for example a
compromised package version in a lockfile) are printed with a remediation hint.
Exit codes: 0 clean, 1 HIGH or CRITICAL findings, 2 error or nothing scanned.
Examples:
pushwarden scan # this directory
pushwarden scan ~/code ~/work # specific directories
pushwarden scan --home --no-system # repositories only
pushwarden scan --home --fix # clean up without questions
pushwarden scan --ci --json report.json # in a pipeline; exit 1 fails the job
pushwarden --deep ~/proj # same as `pushwarden scan --deep ~/proj`
status
Protection status: a score, what each layer of protection is doing, and the details.
pushwarden status
The output has three parts.
The verdict. A score out of 100 with a bar, one word for it and a sentence on what it means:
PROTECTED (90 or more), PARTLY PROTECTED (65 to 89) or AT RISK (below 65). When the latest
sweep found something critical or high the word is THREATS FOUND, whatever the score. On a
terminal the PushWarden shield is drawn beside it in the colour of the verdict; piped or in CI
the same information is plain text.
Protection. One line per layer, marked OK, ` ! (partly) or !!` (missing), with the command
that fixes it:
| Layer | Points | Full marks when |
|---|---|---|
| Background guard | 25 | the guard is alive |
| Real-time file protection | 15 | a native watcher is in use (8 for polling) |
| Starts when you sign in | 10 | the user service is registered |
| Malware servers blocked | 10 | the firewall block is active and persistent (5 if it is lost on restart) |
| Editors cannot auto-run tasks | 10 | every VS Code-family editor found is hardened |
| Indicators up to date | 5 | the indicator file is at most 30 days old |
| Recent full sweep | 5 | a full sweep finished in the last 24 hours |
| Nothing critical or high found | 20 | the latest sweep found none |
The score measures how much of the protection is switched on. It is not a guarantee that the
machine is clean; scan --home and history show what was actually found.
Details. Whether the guard is alive and which real-time backend it uses (inotify, kqueue,
ReadDirectoryChangesW, or polling); during a sweep the Guard line reads scanning 7/17 repositories
(name) and updates as it goes. Then the service state, the installed binary, the time of the
last full sweep, how many repositories are tracked, the plan (Free or Pro, see activate), the indicator version, the configured
action policy, webhook and firewall state, the data directory, the summary of the latest sweep,
and each VS Code-family editor with its task.allowAutomaticTasks setting. Always exits 0.
history and restore
Everything PushWarden has seen and done on this machine: every detection, every kill, quarantine and strip, every answer you gave in a dialog, every error.
pushwarden history [filters] [--all] [--details] [--json]
pushwarden history --restore PATH
pushwarden history --allow PATH [--note "what it really is"]
pushwarden history --remove PATH
pushwarden restore PATH # same as history --restore PATH
The default view has two parts. Detections lists each distinct finding once, with when it
was last seen, how many times (14x), and where it stands: handled, open, allowed, dry-run
or FAILED. Actions and decisions lists what was done, in order, with the reason.
| Option | Effect |
|---|---|
--all |
every recorded event in time order, nothing collapsed: each sighting, sweep, update and error |
--details |
also the exact matched text, the evidence, the command line and which version recorded it |
--severity LEVEL |
only findings at warning, high or critical and above |
--kind KIND |
only this kind of event: finding, action, decision, sweep, guard, update, error, feedback. Repeatable |
--since WHEN |
only newer events: 30m, 24h, 7d, 2w, or a date such as 2026-10-01 |
--path TEXT |
only events whose path, title or command line contains the text |
--archive |
also read the rotated journal archives |
--limit N |
number of rows per part (default 50) |
--json |
the raw events as JSON |
--restore PATH |
put the original file back at PATH. It is still malicious and will be flagged again |
--allow PATH |
restore and stop flagging this exact file content, remembered for 30 days. With --note the reason is recorded as feedback |
--remove PATH |
delete the quarantined copies permanently |
PATH is the original location as shown in the list.
Where it comes from. The journal, journal.jsonl in the data directory, records every sighting
of every finding at WARNING or above, with no deduplication, plus every action, decision, sweep,
update and error. At 10 MB it is rolled into a gzip archive beside it and a new file starts. The
oldest archives are deleted once the archives pass 100 MB in total or are older than a year
(journal_keep_mb, journal_keep_days; see cleanup). Turn it off with pushwarden config --set journal=false; history then shows only the
actions kept in the quarantine index.
Examples:
pushwarden history --severity critical --since 7d # what was serious this week
pushwarden history --all --kind sweep # every sweep with duration and counts
pushwarden history --kind error # anything PushWarden itself got wrong
pushwarden history --all --path A-Bot-Ledger --details
alerts
Recent alerts with the reason for each one. This is what a click on a desktop notification opens.
pushwarden alerts [--last N] [--gui] [--json]
| Option | Effect |
|---|---|
--last N |
number of alerts to show (default 10) |
--gui |
show them in a native dialog instead of the terminal |
--json |
print the raw records from alerts.log |
Each alert shows when it happened, the severity and threat name, the file or process, a why: line
explaining what the evidence means and why it has that severity, and a response: line saying
what was done and why (killed, stripped, quarantined, or left for you to decide).
github-clean
Remove PolinRider from every branch of every GitHub repository you can push to.
pushwarden github-clean [options]
PolinRider steals a token, rewrites your repositories and force-pushes the backdoor to every
branch it can reach. github-clean undoes that at the same scale. For every repository:
- bare-clone it once;
- check out each branch in its own worktree;
- run the repository scanner;
- strip trailing payloads from config files, delete whole-file threats (fake fonts, malicious
.vscode/tasks.json, propagation scripts) and remove the entries PolinRider adds to.gitignoreto hide them, with the same reversible protector asscan --fix; - commit once per infected branch and push with an explicit refspec.
History is never rewritten and nothing is force-pushed. A push the remote refuses (protected branch, archived repository, permission) is reported, not forced.
Dry run is the default. Without --apply nothing is committed or pushed; the output says what
would change on each branch.
Fixing needs Pro. The dry run works in the free version. --apply, and the offer to fix after
a dry run, need a Pro licence; without one, --apply runs as a dry run and says so.
Progress is remembered. Every branch that is finished (clean, fixed and pushed, or flagged for
manual review) is recorded right away, together with the commit it was verified at. If a run is
interrupted with Ctrl-C, hangs, or the machine goes to sleep, run the same command again: branches
whose tip is still that commit are skipped, and a repository where nothing moved is not even
cloned. The summary counts what this run did and what earlier runs did. A branch is checked again
when its tip changes, because the attacker can push again, and when PushWarden or its indicators
are updated, because a newer version may find more. Dry-run findings, refused pushes and errors
are never recorded as done. --progress shows what is remembered; --fresh forgets it and checks
everything.
From dry run to fix without doing the work twice. A dry run in a terminal that finds
infected branches ends with a question: Fix and push these 12 branches in 4 repositories now?
Answer y and the fixes are committed and pushed in the same run. Answer no, and a later
--apply picks up from there:
After a dry run, --apply … |
|
|---|---|
| branches found clean | are skipped, as long as their tip has not moved |
| repositories where every branch was clean | are not cloned |
| repositories with infected branches | are not cloned again: the dry run’s copy is kept in the data directory and only new commits are fetched |
| the infected branches | are scanned once more, then fixed, committed and pushed |
The second scan of an infected branch is deliberate. The fix is made from the files as they are
on the branch at that moment, so nothing stale is ever pushed, and it takes a moment per branch.
A kept copy is a bare repository with no checked-out files and no token. It is deleted as soon
as its repository has nothing left to fix, after 3 days without use (clone_keep_days), when the
copies together pass 2 GB (clone_keep_mb, least recently used first), or by --fresh. A
repository larger than that limit is not kept at all.
| Option | Effect |
|---|---|
--apply |
commit and push the fixes |
--token PAT |
the GitHub token. Also read from GITHUB_TOKEN, then GH_TOKEN, then the gh CLI login |
--token-stdin |
read the token from standard input, for secret managers and CI |
--select |
list the reachable repositories with numbers and pick which to clean and in which order |
--repo owner/name |
only this repository. Repeatable; run in the order given |
--owner NAME |
only repositories under this user or organisation. Repeatable |
--branch GLOB |
only branches matching this glob, for example release/*. Repeatable. Default: all |
--list |
print the repositories the token can push to and exit |
--include-forks |
also clean forks (skipped by default) |
--include-archived |
also clean archived repositories. GitHub rejects pushes to them until they are unarchived |
--author "Name <email>" |
identity for the fix commits. Default: your git config, then PushWarden <pushwarden@users.noreply.github.com> |
--keep-clones DIR |
keep the clones under DIR for inspection instead of deleting them |
--json FILE |
write the full result to FILE |
--api URL |
GitHub Enterprise API base, default https://api.github.com |
--ci |
no colour, no interactive prompts |
--progress |
show what earlier runs already verified, per repository, and exit. Needs no token and no network |
--fresh |
forget the progress of earlier runs, delete the kept copies and check every branch again |
--no-ask |
after a dry run, do not offer to fix what was found. --ci implies it |
The token. Create a fine-grained personal access token with Repository permissions,
Contents: Read and write on the repositories you want cleaned (a classic token needs the
repo scope). The token is handed to git through an askpass helper built into the binary and
an environment variable; it is never written to disk, into a remote URL, or into the report.
Stored git credential helpers, hooks and core.fsmonitor are disabled while git runs.
Selecting repositories. --select prints a numbered list; type 3, 1,4,2, 5-9 or
all. The order you type is the order the repositories are processed, so put the important one
first. --repo does the same non-interactively.
Per-branch status in the output and in the JSON:
| Status | Meaning |
|---|---|
clean |
nothing to do |
clean, unchanged since verified … / fixed earlier … |
taken from an earlier run: the branch is still at the commit that was verified. In the JSON: "resumed": true |
infected |
dry run: this branch needs fixes |
pushed |
fixed and pushed; the short commit hash is shown |
push-failed |
fixed locally, the remote refused the push. The error is shown |
manual |
HIGH or CRITICAL findings PushWarden does not fix automatically. Review them |
error |
clone, worktree, commit or scan failure |
Repository-wide findings about commits in history that touch payload code are listed under
history. They are informational; removing them would need a force-push.
Exit codes: 0 everything clean or fixed and pushed, 1 infected branches remain (dry run,
refused pushes, errors), 2 no token, bad token, or an API failure.
Afterwards. Rotate the token you used and every secret those repositories or their CI could
read. Check Settings, Applications and Deploy keys on GitHub for anything you did not add.
Tell collaborators to git pull. Quarantined originals are in pushwarden history.
Examples:
pushwarden github-clean --list
pushwarden github-clean # dry run over everything
pushwarden github-clean --select --apply # pick from a list
pushwarden github-clean --apply # interrupted? the same command continues
pushwarden github-clean --progress # what is already verified
pushwarden github-clean --repo me/api --repo me/web --apply
pushwarden github-clean --owner my-org --branch main --branch 'release/*' --apply
GITHUB_TOKEN=... pushwarden github-clean --ci --apply --json clean.json
op read op://Vault/GitHub/token | pushwarden github-clean --token-stdin --apply
ui
The easy way to clean your GitHub repositories: guided screens walk you through what github-clean does.
pushwarden ui [options]
There are no commands to remember. The Windows installer adds a PushWarden
entry to the Start menu and pushwarden install on macOS builds ~/Applications/PushWarden.app;
both open a terminal window on these screens. On Linux, run pushwarden ui.
- Sign in. Accounts already on the machine are found: tokens from
GITHUB_TOKEN,GH_TOKENand eachghlogin, and the logins git has over SSH (hosts in~/.ssh/configthat lead to github.com). With several you choose from a list. With none, the screen explains how to create a token and takes it in a masked field; it is used for this run only and never written to disk.son the repository list or the final report switches to another account. - Choose repositories. A list with checkboxes, nothing ticked to begin with. Each row shows
what earlier runs verified and whether the repository is cloned on this computer. Forks and
archived repositories are hidden until you show them. A token account lists every
repository it can push to. An SSH account cannot ask GitHub for that list: it shows the
local clones that use its SSH host, the account’s public repositories, and names you add
with
+. - Check. A dry run over what you chose. Nothing is changed.
- Review. Every infected branch and the files that would be fixed.
- Fix and push, only after you answer
y. One normal commit per infected branch, never a force-push, exactly asgithub-clean --apply. This step needs Pro; in the free version the review ends with what would be fixed and how to unlock it.
Stopping with q or Ctrl+C keeps what is finished: the next run continues where this one
stopped, and shares that progress with github-clean.
| Option | Effect |
|---|---|
--api URL |
GitHub API base URL (GitHub Enterprise). |
--fresh |
Forget the progress of earlier runs and check every branch again. |
--pause |
If it cannot start, wait for Enter before closing. The shortcuts use this. |
Exit codes: as github-clean; 2 also when there is no interactive terminal.
For scripts and CI use github-clean.
feedback
Package this machine’s activity into a file you can read and send by hand, or report that a finding was wrong.
pushwarden feedback [--days N] [--out FILE] [--no-redact]
pushwarden feedback --false-positive PATH [--note "what it really is"]
| Option | Effect |
|---|---|
| (none) | write pushwarden-feedback-DATE.zip in the current folder: the journal for the last 14 days, the tail of the guard log, the latest report, the configuration and a status summary |
--days N |
how many days of activity to include |
--out FILE |
where to write the bundle |
--no-redact |
keep paths, user and host names. Token-shaped strings are masked regardless |
--false-positive PATH |
record that the finding on PATH was wrong, with an optional --note. Does not change the file; history --allow does that |
Nothing is sent anywhere. The command writes a file and tells you what is in it, so you can look before you send it.
Redaction, on by default. The home folder is written as ~, the user name and host name are
removed, and anything shaped like a secret is masked: GitHub, AWS, Slack and API tokens, private
keys, JSON Web Tokens, credentials inside URLs, and the values of token, password, api_key,
webhook_url and similar keys.
cleanup
Show what PushWarden occupies on disk and remove what is past its limits.
pushwarden cleanup [--dry-run]
PushWarden is meant to run for months without attention, so nothing it stores may grow without
bound. Every store in the data directory has a limit. The guard enforces them once a day;
this command does the same on demand and prints the sizes. --dry-run shows what would be
removed and removes nothing. pushwarden status shows the total in its Disk use line.
| Store | Limit | Setting |
|---|---|---|
journal archives (journal-*.jsonl.gz) |
100 MB in total and 365 days; oldest deleted first. The active journal.jsonl rotates at 10 MB |
journal_keep_mb, journal_keep_days |
| quarantined originals | 90 days and 500 MB; oldest deleted first. history then shows the entry as expired, and it can no longer be restored |
quarantine_keep_days, quarantine_keep_mb |
repository copies kept by github-clean |
3 days without use and 2 GB in total | clone_keep_days, clone_keep_mb |
github-clean progress |
repositories not seen for 90 days are forgotten | |
| guard log | rotates at 5 MB, 5 archives kept | |
| alerts log | rotates at 5 MB, 3 archives kept | |
| reports | newest 60 | report_keep |
| allow decisions | removed when they expire after 30 days | |
editor settings backups (settings.json.pushwarden-*.bak) |
newest 2 per file | |
leftovers in the system temp folder from an interrupted github-clean |
removed after a day |
Set a limit to 0 to turn it off, for example pushwarden config --set quarantine_keep_days=0
to keep quarantined files until you remove them yourself.
guard
Run the background protection loop in this terminal. install registers this as a user
service; run it by hand to debug or on a machine without a service manager.
pushwarden guard [--once] [--dry-run] [--verbose]
| Option | Effect |
|---|---|
--once |
one full pass, then exit |
--dry-run |
detect and alert but never kill or quarantine |
--verbose |
print the log to the terminal as well as to guard.log |
The loop runs four layers, each timed by a configuration key:
| Layer | Work | Key |
|---|---|---|
| real-time | native file-system watcher over the project roots, ~/Downloads and ~/Desktop; each written file is scanned as it lands |
realtime |
| quick | processes and network connections to C2 addresses | quick_interval (5 s) |
| full | every repository plus host persistence, RATs, editor injection, credentials | full_interval (6 h) |
| definitions | download and validate iocs.json |
ioc_update_interval (24 h) |
Findings go through the action policy (see Configuration keys), then
through desktop notification and webhook. Configuration is read at start: restart the guard after
pushwarden config --set.
install
Install the background guard so it starts at sign-in, harden editors, run a first scan.
pushwarden install [options]
Everything is per user: the binary is copied to a per-user install directory and linked from
~/.local/bin. When that folder is not on your PATH (the macOS default), one line is appended to
your shell start-up file (~/.zshrc on macOS, ~/.bashrc or ~/.profile on Linux), marked
# added by pushwarden install; open a new terminal afterwards. The guard is registered as a
systemd --user service, a LaunchAgent or a Scheduled Task,
and every VS Code-family editor found gets task.allowAutomaticTasks = off and workspace trust
turned on.
| Option | Effect |
|---|---|
--roots DIR |
project directory to watch. Repeatable. Default: auto-discover the usual places under your home folder |
--webhook URL |
URL that receives JSON alerts (Slack, Discord, Teams or your own endpoint) |
--no-block-c2 |
do not ask for administrator rights to block the C2 servers (default is to ask once; see protect) |
--server |
install for a server (Linux, run with sudo): a systemd system service that starts at boot and runs as root, program in /usr/local/lib/pushwarden, data in /var/lib/pushwarden, no dialogs or desktop notifications, no editor hardening, /srv, /var/www, /opt and /usr/local/src watched when they exist and --roots is not given. On macOS and Windows it only applies the headless settings. See Running on servers |
--report-only |
with --server: set action=report, so malware is reported and never moved or changed |
--deep |
the guard also scans node_modules and vendor (slow) |
--full-interval SECONDS |
seconds between full sweeps (default 21600) |
--no-harden |
skip editor hardening |
--npm-ignore-scripts |
also set ignore-scripts=true in ~/.npmrc |
--no-kill |
never kill processes automatically |
--no-prompt |
never show dialogs; quarantine automatically |
--no-clean |
when no dialog can be shown, leave files in place instead of quarantining |
--unattended |
for installers: no questions, first scan in the background |
--dry-run |
show what would be done and change nothing |
On macOS it also builds ~/Applications/PushWarden.app, a shortcut that opens ui in Terminal;
the Windows installer adds the same shortcut to the Start menu.
The one-line installer from the README calls this for you; its environment variables
PUSHWARDEN_ROOTS, PUSHWARDEN_WEBHOOK, PUSHWARDEN_SERVER, PUSHWARDEN_REPORT_ONLY,
PUSHWARDEN_VERSION and PUSHWARDEN_NO_INSTALL map onto these options.
With a system service installed, sudo pushwarden ... uses /var/lib/pushwarden (the service’s data),
while an ordinary user keeps their own ~/.pushwarden. uninstall removes whichever service exists,
and needs sudo for the system one.
uninstall
Remove the background guard.
pushwarden uninstall [--unblock] [--purge]
| Option | Effect |
|---|---|
--unblock |
also remove the C2 block: the boot-time job, the firewall rules and the hosts entries (asks for administrator rights) |
--purge |
also delete the data directory: config, reports, quarantine |
Editor hardening is left in place because it is a safe default.
update
Update PushWarden to the latest release.
pushwarden update [--check]
--check only reports whether a newer release exists. On a terminal the download shows a
progress bar with the megabytes received and the percentage. Releases are verified before they replace
the binary: the checksums file is signed with the project’s ed25519 key and the download’s
SHA-256 must match. The previous binary is kept; if the new one fails to start the guard rolls
back. The guard does this on its own every update_interval seconds when auto_update is on.
update_channel beta also considers pre-releases.
update-iocs
Download the latest indicator file.
pushwarden update-iocs [--url URL]
The default source is the file on the project’s main branch. The download is parsed and
validated (every regex must compile, every IP and hash must be well formed) before it replaces
the previous copy, and a file whose version is not newer is ignored. A running guard picks a new
file up at its next definitions check; restart the service to use it immediately.
harden
Apply preventive settings without installing the guard.
pushwarden harden [options]
| Option | Effect |
|---|---|
| (none) | harden every VS Code-family editor found: task.allowAutomaticTasks = off, workspace trust on |
--npm-ignore-scripts |
set ignore-scripts=true in ~/.npmrc, so installs never run lifecycle scripts |
--undo-npm |
remove that setting again |
--pre-commit REPO |
install a pre-commit hook in REPO that runs pushwarden check-staged. Repeatable. An existing hook is kept as pre-commit.pre-pushwarden |
--dry-run |
show what would change |
protect
Block the PolinRider command-and-control (C2) servers system-wide and keep them blocked.
pushwarden protect [--install | --refresh | --uninstall | --status | --block-c2 | --unblock] [--dry-run]
pushwarden install does this for you: it asks once for administrator rights (native password
dialog on macOS, polkit or sudo on Linux, UAC on Windows) and registers a small privileged job
that re-applies the block at every boot and refreshes the address list once a day. You only need
protect directly to check on it, to add it later, or to remove it.
| Option | Effect |
|---|---|
--install (default) |
block now and register the boot-time job. Needs administrator rights; asks for them when run as a normal user |
--status |
is the block active, and will it survive a reboot? No rights needed |
--refresh |
what the job runs: re-apply the rules from the root-owned indicators, then fetch newer ones and re-apply |
--uninstall |
remove the job, the firewall rules and the hosts entries |
--block-c2 |
one-shot block for this boot only, no job (the pre-0.3 behaviour) |
--unblock |
remove the rules and hosts entries but keep the job, if any |
--dry-run |
show what --install would do |
What gets installed. Outgoing traffic to every IP on the C2 list is dropped: an iptables
chain or nftables table named pushwarden on Linux, a pf anchor on macOS, a Windows Firewall
rule named “PushWarden C2 block”. The C2 hostnames are sinkholed in the hosts file between
# BEGIN PUSHWARDEN C2 SINKHOLE and # END markers. The job that keeps this current is a systemd
timer (pushwarden-netblock.timer, at boot and daily), a LaunchDaemon (com.pushwarden.netblock)
or a Scheduled Task (“PushWarden NetBlock”, runs as SYSTEM at boot and daily).
Why it uses its own copy of the indicators. The job runs as root, and the iocs.json under
your home folder is writable by anything running as you. A privileged job must not let a
user-writable file decide what goes into the hosts file, so it keeps a root-owned copy under
/etc/pushwarden, /Library/Application Support/PushWarden or %ProgramData%\PushWarden,
refreshed from the project repository only, and runs a root-owned copy of the program.
Status values shown by pushwarden status and protect --status:
| Firewall | Meaning |
|---|---|
active (persistent; 25 IPs, 15 hosts; applied 3h ago; indicators …) |
all good |
active until reboot (…) |
a one-shot block from --block-c2; run protect --install to keep it |
not active (rules from a previous boot were lost) |
same, after a reboot |
not active |
never installed, or removed |
Opt out with pushwarden install --no-block-c2 or pushwarden config --set block_c2=false; the
guard then stops reminding you. Setting PUSHWARDEN_NO_BLOCK=1 skips the administrator prompt for
one run, which the package installers use because they already did the work as root.
config
Show or change settings.
pushwarden config
pushwarden config --set key=value [--set key=value ...]
Without options it prints the current configuration. Keys are the JSON names listed under
Configuration keys. Booleans take true/false, lists are
comma-separated. The guard reads the file when it starts, so restart the service after a change
(systemctl --user restart pushwarden-guard.service on Linux).
pushwarden config --set action=ask
pushwarden config --set scan_roots=~/code,~/work
pushwarden config --set webhook_url=https://hooks.slack.com/services/...
pushwarden config --set auto_update=false
check-staged
Pre-commit hook helper: scan the files staged for commit and refuse the commit if any carries a PolinRider indicator at HIGH or above.
pushwarden check-staged
Run it from inside the repository. Exit 1 blocks the commit, 0 allows it. Install it with
pushwarden harden --pre-commit ., or add this to .git/hooks/pre-commit yourself:
#!/bin/sh
pushwarden check-staged || exit 1
activate
Unlock PushWarden Pro on this computer with a licence key.
pushwarden activate PW-XXXXX-XXXXX-XXXXX-XXXXX
pushwarden activate # uses $PUSHWARDEN_LICENSE_KEY, or asks you to paste the key
A key may be used on a limited number of devices. activate sends the key and a description of this
computer (a hash that identifies it, host name, operating system, processor type and program
version) to the licence server over HTTPS and saves the signed licence it gets back in
licence.json in the data directory (mode 0600). It prints how many of the key’s devices are now in
use. Activating a computer that is already registered costs nothing. A key that is used on all its
devices is refused with a message that says so (it names none of the other computers); the key’s
owner frees a place.
The running guard notices a new licence without a restart. Exit code 0 activated, 1 refused or
the server could not be reached, 2 no key given.
The licence server is licence.ServerURL built into the program; licence_url in the settings or
PUSHWARDEN_LICENSE_URL points somewhere else. It must be https:// (plain http:// only for
localhost).
Pro unlocks two things:
- removing only the malicious part of an infected config or entry file, and the hidden entries
PolinRider adds to
.gitignore(free: the file is quarantined whole); - fixing and pushing with
github-clean --applyand the guided screens (free: check only).
Everything else works the same in both versions. See Free and Pro.
licence
Show the plan of this computer, ask the licence server again, or remove the saved key.
pushwarden licence # plan, licensed to, devices in use, when it was last confirmed
pushwarden licence --refresh # ask the licence server now, then show the plan
pushwarden licence --remove # delete the saved key: back to the free version
pushwarden license is the same command. The guard asks the server to confirm the key about once a
day; a revoked, expired or removed licence ends Pro at that check. If the server cannot be reached,
Pro keeps working for 7 days after its last answer, then the program works as the free version until
it can reach the server again (licence --refresh). PushWarden also remembers the latest time its
clock showed while the licence was in force: a clock more than an hour earlier than that, or earlier
than the server’s last answer, switches Pro off, so turning the clock back adds no offline days
(licence --refresh resets the record from the server’s time when the clock is right). --remove deletes the key from this computer
only: the computer keeps its place on the key, so activating again here costs nothing.
version
pushwarden version
pushwarden --version
Prints the version. Release builds print the release number; development builds end in -dev.
help
Show this documentation in the terminal.
pushwarden help # command list
pushwarden help <command> # one command, for example: pushwarden help install
pushwarden help <topic> # config-keys, files, environment, severities, recipes, troubleshooting
pushwarden help all # the whole reference
The text is embedded in the binary, so it always matches the installed version and works
offline. Long pages go through $PAGER (default less); set PUSHWARDEN_NO_PAGER=1 to print
straight to the terminal.
Configuration keys
Stored in config.json in the data directory. Change them with pushwarden config --set.
| Key | Default | Meaning |
|---|---|---|
scan_roots |
[] |
directories the guard watches and scan --home covers. Empty means auto-discover |
exclude |
[] |
directories never scanned |
js_all |
true |
scan every script file, not only known config names |
deep |
false |
also scan node_modules and vendor |
realtime |
true |
real-time file watcher on |
quick_interval |
5 |
seconds between process and network checks |
full_interval |
21600 |
seconds between full sweeps |
ioc_update_interval |
86400 |
seconds between indicator downloads |
ioc_update |
true |
download indicators at all |
ioc_update_url |
"" |
alternative indicator URL, for mirrors |
action |
quarantine |
what the guard does with a CRITICAL file, see below |
auto_kill |
true |
kill processes that match a kill-list indicator |
auto_clean |
true |
with action=ask, quarantine when nobody answers the dialog |
prompt |
true |
show native dialogs |
prompt_timeout |
180 |
seconds a dialog waits before the default applies |
notify_desktop |
true |
desktop notifications |
notify_min_severity |
HIGH |
lowest severity that notifies |
notify_sweeps |
true |
desktop notification when a background full sweep starts and when it finishes, with the result |
webhook_url |
"" |
receives a JSON POST per alert |
webhook_min_severity |
HIGH |
lowest severity that posts |
block_c2 |
true |
keep the system-wide C2 block installed; install asks for administrator rights once and the guard reminds you daily while it is missing |
report_keep |
60 |
number of reports kept |
journal |
true |
record every finding, action, decision, sweep and error in journal.jsonl |
journal_min_severity |
WARNING |
lowest finding severity recorded in the journal |
journal_keep_mb |
100 |
journal archives kept, in total; the oldest are deleted first. 0 = no limit |
journal_keep_days |
365 |
oldest journal archive kept. 0 = no limit |
quarantine_keep_days |
90 |
quarantined originals are deleted after this many days. 0 = keep until removed by hand |
quarantine_keep_mb |
500 |
size of the quarantine folder; the oldest copies are deleted first. 0 = no limit |
clone_keep_days |
3 |
how long github-clean keeps a repository copy waiting for --apply |
clone_keep_mb |
2048 |
those copies in total; a larger repository is not kept |
auto_update |
true |
update the binary automatically |
update_channel |
stable |
stable or beta |
update_interval |
21600 |
seconds between update checks |
update_api_url |
"" |
alternative release API, for mirrors |
licence_url |
"" |
the PushWarden Pro licence server, when it is not the built-in one |
action values:
| Value | Behaviour |
|---|---|
quarantine |
move the file to quarantine first (reversible), then show a dialog: remove for good, or restore and allow |
ask |
show the dialog first; on timeout quarantine if auto_clean is on |
delete |
remove without asking. Stripped files keep a backup in quarantine; deleted files do not |
report |
alert only, never touch files. Processes are still killed if auto_kill is on |
Files and directories
The data directory is ~/.pushwarden (override with PUSHWARDEN_HOME). If only ~/.threatscan
exists, from when the program was called ThreatScan, that folder is used instead.
| Path | Content |
|---|---|
config.json |
settings |
licence.json |
the PushWarden Pro key and the signed licence for this computer, saved by activate; removed by licence --remove |
iocs.json |
downloaded indicators; the binary carries an embedded copy as fallback |
journal.jsonl |
the complete activity record: every finding at every sighting, every action, decision, sweep, update and error. Rotated at 10 MB into journal-DATE.jsonl.gz; the oldest archives are deleted past 100 MB or one year, leaving a tiny journal-DATE.pruned marker with their event count |
guard.log |
the guard’s text log, rotated at 5 MB into guard-DATE.log.gz; the newest 5 archives are kept |
machine-id |
a random identifier for this install, used as the device identity for Pro on systems that have no machine id of their own |
github-clean-state.json |
what github-clean already verified: per repository and branch the commit, the result and the time. No token. Deleted by github-clean --fresh |
github-clean-clones/ |
bare copies of repositories in which a github-clean dry run found infected branches, kept so --apply does not download them again. Removed when fixed, after 3 days without use, past 2 GB in total, or by github-clean --fresh |
alerts.log |
one JSON line per alert, including the why and response texts as worded at the time. Rotated at 5 MB; the newest 3 archives are kept |
reports/ |
timestamped JSON reports plus latest.json |
quarantine/ |
copies of every stripped or deleted file, and index.jsonl, the protection history. Copies are deleted after 90 days or past 500 MB |
decisions.json |
files you chose to allow, with their content hash; entries are removed when they expire after 30 days |
PushWarden Notifier.app |
macOS only, in the install directory: the helper that posts notifications so a click opens pushwarden alerts --gui |
guard/ |
heartbeat and state of the running guard |
The privileged C2 block keeps its own root-owned files: netblock-state.json and a copy of
iocs.json under /etc/pushwarden (Linux), /Library/Application Support/PushWarden (macOS) or
%ProgramData%\PushWarden (Windows), plus a copy of the program under /usr/local/lib/pushwarden,
/usr/local/libexec/pushwarden or %ProgramFiles%\PushWarden.
The binary itself lives in a per-user install directory, on Linux ~/.local/share/pushwarden
with a link in ~/.local/bin (override with PUSHWARDEN_INSTALL_DIR).
Environment variables
| Variable | Read by | Effect |
|---|---|---|
PUSHWARDEN_HOME |
every command | data directory instead of ~/.pushwarden |
PUSHWARDEN_INSTALL_DIR |
install, update |
where the binary is installed |
GITHUB_TOKEN, GH_TOKEN |
github-clean |
token when --token is not given |
PUSHWARDEN_LICENSE_KEY |
activate, the guard |
a PushWarden Pro key; the guard activates it once if none is saved |
PUSHWARDEN_LICENSE_URL |
activate, licence, the guard |
the licence server, instead of licence_url and the built-in one |
NO_COLOR |
every command | disable coloured output |
PAGER, PUSHWARDEN_NO_PAGER |
help |
pager for long pages; set the second to disable paging |
PUSHWARDEN_NO_BLOCK |
install, protect, uninstall |
never ask for administrator rights (package installers, CI) |
PUSHWARDEN_ROOTS |
install script | space-separated project directories to watch |
PUSHWARDEN_WEBHOOK |
install script | webhook URL |
PUSHWARDEN_VERSION |
install script | install this release instead of the latest |
PUSHWARDEN_NO_INSTALL |
install script | download the binary only, do not register the guard |
PUSHWARDEN_BASE_URL |
install script | download from a mirror |
Severities, actions and threat names
| Severity | Meaning | Automatic action |
|---|---|---|
| CRITICAL | confirmed malware artifact or activity: a PolinRider indicator, or code that runs by itself and downloads or hides what it runs | yes, per action policy, when the file can be quarantined or stripped |
| HIGH | strong indicator that needs a human: compromised package version, exposed keys on an infected host | no, listed for review |
| WARNING | context worth checking: suspicious git reflog, secrets file in an infected repo, files that arrived in the same commit as a loader (camouflage fonts, decoy README, .vscode set) |
no |
| INFO | informational | no |
Why-lines. Every finding has a one-sentence reason for its severity, and every response
(kill, strip, quarantine, or nothing) has a one-sentence reason too. They appear in scan output,
in the dialogs, in pushwarden alerts, in pushwarden history, in desktop notifications, in the
webhook JSON (reasons array) and in guard.log. A process is killed only when its command line
holds a strict PolinRider marker that legitimate tools never use; a broad indicator alone is
reported but not killed, and the reason says so.
Dialogs, notifications and reports name threats Defender-style, for example
Trojan:JS/PolinRider.FakeFont for a script disguised as a font, Trojan:Script/PolinRider for an
injected loader and Backdoor:Script/PolinRider.Persist for a persistence entry. Generic rules,
which catch the same tricks from other campaigns, use Generic names:
Trojan:Script/Generic.AutoRun (a folder-open task or dev container that downloads code),
Trojan:Script/Generic.Downloader (a git hook or install script that downloads and runs code),
Trojan:JS/Generic.HiddenCode (code hidden off-screen behind whitespace) and
Trojan:JS/Generic.Obfuscated (an obfuscated loader in a hand-written file).
Recipes
CI gate. Fail the pipeline when a PolinRider indicator lands in the repository:
- run: curl -fsSL https://pushwarden.com/install.sh | PUSHWARDEN_NO_INSTALL=1 sh
- run: ~/.local/bin/pushwarden scan --ci --no-system --json pushwarden.json .
Pre-commit hook for every repository you work on:
for r in ~/code/*/; do pushwarden harden --pre-commit "$r"; done
Slack, Discord or Teams alerts from every machine:
pushwarden config --set webhook_url=https://hooks.slack.com/services/T000/B000/XXXX
The POST body contains text (Slack), content (Discord), host, platform, context and
the findings array, so one URL works for all three and for your own endpoint.
Organisation-wide clean-up after a compromise, from a machine with a fine-grained token that has Contents write on the organisation’s repositories:
pushwarden github-clean --owner my-org --list
pushwarden github-clean --owner my-org --json dryrun.json # review dryrun.json
pushwarden github-clean --owner my-org --apply --json applied.json
Machine audit without touching anything:
pushwarden scan --home --no-prompt --verbose --json audit.json
Troubleshooting
Clicking a macOS notification opens Script Editor. Builds before 0.3 posted notifications
through osascript. Run pushwarden install again: it builds the notification helper app, and
macOS will ask once whether PushWarden may send notifications. Allow it.
A legitimate file was quarantined. pushwarden history shows it; pushwarden history --allow
PATH restores it and stops flagging that exact content. Please also open an issue with the file so
the indicator can be tightened.
A process is flagged repeatedly. The alert in alerts.log shows the indicator and the command
line. Update indicators first, since false positives are fixed there without a new release:
pushwarden update-iocs and then restart the service (systemctl --user restart
pushwarden-guard.service on Linux). If it persists, open an issue with the alert line.
Firewall says not active or until reboot. The one-time administrator prompt was declined
or could not be shown (for example a headless server without polkit). Run sudo pushwarden protect
--install on Linux and macOS, or pushwarden protect --install from an elevated terminal on
Windows. pushwarden protect --status confirms it afterwards.
status says the guard is not alive. Check the service: systemctl --user status
pushwarden-guard.service on Linux, launchctl list | grep pushwarden on macOS, Task Scheduler
on Windows. Run pushwarden guard --verbose --once in a terminal to see errors directly.
Real-time shows off or polling. On Linux the inotify watch limit may be exhausted with many
large repositories; raise fs.inotify.max_user_watches. The guard falls back to polling and to the
5-second and 6-hour layers, so protection continues.
github-clean says push-failed. The branch is protected, the repository is archived, or the
token lacks write access. Temporarily allow the push (or add yourself to the bypass list), or
unarchive the repository, then run again for that repository with --repo owner/name --apply. A
non-fast-forward error means someone pushed between clone and push; simply run again.
github-clean cannot find a token. Pass --token, export GITHUB_TOKEN, or run gh auth
login. A fine-grained token must list the repositories and grant Contents: Read and write.
Uninstall completely: pushwarden uninstall --unblock --purge, then delete the install
directory shown by pushwarden status.