Repository navigation
Releases
v0.5.18
Daily reports while a machine is non-compliant. While the last check found a fail, or signs of compromise, the machine reports every 24h instead of every report.interval (e.g. weekly), so IT sees each day whether it was fixed; once the fails are gone it goes back to interval. Warnings alone ("attention") keep interval. Nothing to set: it is the default. Policy report.fail_interval changes it ("0" = always interval; at least 1h); setting that field needs v0.5.18 or later on every machine of the role. bordercheck status shows it.
bordercheck guard: a here-document (cmd <<'EOF' … EOF) is read as the data it is, not as commands, so a script whose text mentions export or env is no longer refused; $( ) and backticks in an unquoted one, and commands after it, are still checked.
Both release keys are still trusted (see v0.5.17); the next release is the first signed with the new key only.
v0.5.17
Security
- A provider token written as a JSON key or before a colon (
"ghp_…": …,ghp_…:x-oauth-basic) was taken for the variable's name, and the whole token could reach the report as that name. A name that looks like a token is now dropped. - New release key. SHA256SUMS will be signed on the maintainer's Mac with a key kept behind Touch ID, never in CI. This release trusts the new key and the previous one; the next release is the first signed with the new key and trusts it alone, and the CI secret is then deleted. Keep installed copies updating (the default) so they follow.
- On macOS, an update must be signed by the same Developer ID team as the running copy before it runs, and
install.shchecks the binary's Developer ID before running it. - Rulesets now forbid moving or deleting release tags and force-pushing
main.
bordercheck full-scan -verify asks each provider whether the tokens found still work (GitHub, OpenAI, Anthropic, Stripe, Slack, npm): each token goes only to the provider that issued it, with a read-only request that costs nothing, and each finding then says LIVE, dead or unknown. Off unless asked for; the hourly check never does it.
Docs: a threat model (docs/threat-model.md) and a security policy (SECURITY.md): private reporting is on, with a US$100 bounty for confirmed vulnerabilities in scope. The README no longer says that no location is ever sent (include_location can send one).
v0.5.16
Android and Java signing keys. The secrets check now:
- reads signing
.propertiesfiles (keystore.properties,key.properties,gradle.properties,local.properties, and others whose name mentions key, sign, keystore, release or upload) and~/.gradle/gradle.properties, so astorePassword/keyPasswordkept in plain text is reported, with advice to keep it in touchenv and give it to Gradle as an environment variable (touchenv run --intent "release" STORE_PASSWORD KEY_PASSWORD -- ./gradlew bundleRelease); - lists Java/Android keystores (
.jks,.keystore,.bks) for information: a keystore is safe while its passwords are not in a file and it stays out of repositories and synced folders. One in the Trash or a synced folder (iCloud Drive, Dropbox, OneDrive, Google Drive) counts as a finding. The Android SDK'sdebug.keystore, whose password everyone knows, is skipped.
v0.5.15
bordercheck full-scan (it replaces bordercheck chats, which still works) reads what the hourly check reads only in part or not at all, and lists every secret it finds, never the values:
- all of the AI chat history (Claude Code, Codex, Gemini CLI, Copilot, Cursor's agent, VS Code chat, Cline, Aider);
- the databases of tools that keep chats or a clipboard: Cursor's editor, opencode, Goose, Alfred's clipboard history;
- log files (
.login the temporary folders and~/Library/Logs); - the history of your own commits in every git repository the secrets walk reaches, on every branch and tag, and the stash: which commit added each secret, whether it was pushed and whether the current files still hold it. Commits by other authors, dependencies and build output committed by mistake, tools' cached repositories and made-up passwords in test and example files are left out.
-no-gitskips it.
It takes about a minute on a busy computer. It only reads, apart from noting the date it ran (fullscan.json in the install folder). Until it has run once, the secrets summary suggests it, and a notification nudges the person: a day after install, then every 14 days, never again once it has run. Policy: notify.first_full_scan_nudge ("24h", "0" = never), notify.full_scan_nudge_every ("336h", "0" = only once).
More places the secrets check reads (policy checks.secrets.locations):
trash: the Trash (macOS~/.Trash, Linux, and the Windows Recycle Bin with each file's original name): keys,.envfiles and SSH private keys left there stay on disk until it is emptied.key_files: Google service account keys saved anywhere, such as~/Downloads.api_clients: Insomnia's environments and request credentials, exported Postman environments, DBeaver's saved passwords.
Each comes with its own advice. A secret in a folder synced with a cloud service (iCloud Drive, Dropbox, OneDrive, Google Drive) is marked [synced to …], with advice to delete the old versions there too.
New policy fields need v0.5.15 or later on every machine of the role when set. The Windows Recycle Bin and the full scan on Windows are covered by tests only.
v0.5.14
The same code as v0.5.13, released again with what v0.5.13 lacked: its macOS binaries are signed with Developer ID and notarized, and the notification app is included. (v0.5.13's macOS binaries were only signed ad hoc, because notarization did not run; they work when installed with install.sh or Homebrew and when BorderCheck updates itself, and v0.5.13 had no notification app, so Macs that updated to it kept their current one.)
From v0.5.13: cloud CLI sign-ins are information. Azure CLI's token cache and gcloud's credential store and application default credentials are kept in plain text by the CLIs by design, and signing out revokes them. The secrets check lists them for information, with how to sign out (az logout, gcloud auth revoke, gcloud auth application-default revoke) in its summary, instead of warning. Long-lived secrets (a service principal's secret, a service account key file) still warn. Policy: checks.secrets.cli_sign_ins_info (default true).
v0.5.13
v0.5.12
Claude Code guard. The hook that refuses commands printing secrets wholesale now reads a command the way the shell does: only the command word and its arguments count, so the same words inside a quoted string (a commit message, an echo) no longer get a command refused. Commands inside $(…) and backquotes are still checked, and sudo, exec, time and nohup in front of one are looked past. Found because v0.5.11's hook refused a commit whose message mentioned touchenv get.
v0.5.11
Fixes that fit. When the secrets check finds a credential store in plain text, its fix now says what to do for that tool instead of "move it to the keychain": az logout for Azure CLI's token cache, gh auth login to put a GitHub CLI token in the keychain, a credential helper for git and Docker, gcloud auth application-default revoke, terraform logout, an exec plugin for kubeconfig tokens, and so on. The general advice stays when other secrets are found too.
Claude Code guard. On a computer where Claude Code is used and bordercheck guard install has not set up all three hooks, the secrets summary suggests it, whether or not a secret was found.
AI chat history.
- Fewer false alarms: a "password" that is code (another variable, a field, a query string) and a URL whose host is a pattern or an example (
host,*.example,.test) are no longer reported. bordercheck chatslists every line a repeated secret is on (linesin-json), so deleting it leaves no copy behind.
v0.5.10
AI chat history.
- The
secretscheck also reads what was typed into AI assistants' prompts, and the last 48 hours of their chat history: Claude Code, Codex, Gemini CLI, GitHub Copilot (CLI and chat in VS Code and the editors built on it), Cursor's agent, Cline / Roo Code / Kilo Code, and Aider. What it finds there is for information only: it never makes the check warn or fail. Images and other encoded strings are skipped. Policy:locations.ai_chats,ai_chat_hours(48),ai_chat_max_mb(256). bordercheck chatslists every secret in all of that history: the item (kind, variable name, last 4 characters), where (file and line) and what it is (assistant, project, date, typed / sent / written by the assistant / printed by a command, and what the secret allows). Values are never printed, and none of it goes into a report.-hours N,-json. Exit status 1 when it finds something.
Keeping secrets out of Claude Code. bordercheck guard install adds three Claude Code hooks: a prompt that holds a secret is stopped before it is sent (#allow-secret lets it through); a command that prints secrets wholesale (env, cat .env, touchenv get…) is refused, with what to do instead; secrets in a command's output are replaced with ***REDACTED*** before the model sees them. Only the hooks' own entries in ~/.claude/settings.json are added or removed. bordercheck guard shows them, bordercheck guard uninstall removes them. Nothing sets them up on its own; the secrets summary and bordercheck chats suggest it.
Not read: Cursor's editor chats, opencode and Goose (SQLite), Windsurf (protobuf), Codex sessions compressed to .jsonl.zst. The hooks were tried with Claude Code on macOS; on Windows they are covered by unit tests only.
v0.5.9
Updates. What a new version adds around the program (the plain bordercheck command, the entry in the computer's app search, the app on a Mac) is now set up as soon as an update has installed it, instead of on the next hourly run: the version being replaced runs the new one once. bordercheck update also puts these in place when there is nothing new to install. This takes effect from the update after this one.