Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ permissions:
contents: read

env:
AIH_VERSION: 2.0.1
AIH_VERSION: 2.1.0

jobs:
security-gate:
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ Newest first. One line per change. Default-behavior changes marked **CHANGED**.
### CI
- **CHANGED:** the security gate (`.github/workflows/gate.yml`) runs the latest stable `am-i-hacked` release from npm, pinned to an exact version, instead of the unreleased scanner on `main` (#6).

## 2026-10-09 [am-i-hacked 2.1.0]

**BLUF:** A security release: a scanned repository's `.git/config` can no longer make `am-i-hacked` or `safe-pull` run a command, and payloads in dot-directories, ignored folders and committed build output are now read. It also adds the dark-corner system scan, Python virtualenv integrity and `--max-findings`. There was no 2.0.1; its changes are in 2.1.0. The package changelog, [`apps/am-i-hacked/CHANGELOG.md`](apps/am-i-hacked/CHANGELOG.md), has the detail.

### am-i-hacked
- Security: the scan and `safe-pull` run git with `core.fsmonitor=false`, so a `.git/config` that ships inside an archive cannot run a command.
- **CHANGED:** the scan reads dot-directories, ignores `.ignore` and `.rgignore`, and scans tracked files that `.gitignore` matches and committed build output. Findings may appear in `dist/`, `.github/`, `.vscode/` and similar.
- **CHANGED:** 71 official Yarn releases in `.yarn/releases` are verified by SHA256.
- **CHANGED:** bundled code gets only high-signal checks, and embedded WebAssembly data: URLs are not flagged for line length.
- `--system` also scans a fixed list of dark corners (virtualenvs, package-manager caches, language bin folders), and reports a check it could not run instead of passing it.
- The folder scan verifies Python virtualenv integrity against each package's `RECORD` hashes.
- `--max-findings N` (default 1000, was 100). **CHANGED:** over the cap, the true total and a per-rule count are printed and higher-severity findings are kept first.
- **CHANGED:** hex and unicode escape findings inside installed dependency folders are one summary count. `bin/host-audit.sh` is now `bin/system-scan.sh`, with the old name kept as a shim.

### CI
- The security gate pins `am-i-hacked` 2.1.0.

## 2026-10-05

### Skills
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ npx am-i-hacked
[![npm am-i-being-recorded](https://img.shields.io/npm/v/am-i-being-recorded?label=am-i-being-recorded)](https://www.npmjs.com/package/am-i-being-recorded)
[![npm secure-semgrep](https://img.shields.io/npm/v/secure-semgrep?label=secure-semgrep)](https://www.npmjs.com/package/secure-semgrep)

**Status:** actively maintained, macOS and Linux. Current releases: `am-i-hacked` 2.0.1, `am-i-being-recorded` 1.0.0, `secure-semgrep` 1.0.1. The next `am-i-hacked` release adds a dark-corner system scan, a Python virtualenv integrity check and `--max-findings`; each package's changelog has the detail. Website: <https://isaacbell.github.io/secure-devtools/>, with [guides](https://isaacbell.github.io/secure-devtools/guides/) such as [how to inspect an untrusted repository](https://isaacbell.github.io/secure-devtools/guides/inspect-untrusted-repository/) and [how to check AI-generated code](https://isaacbell.github.io/secure-devtools/guides/ai-generated-code-security/).
**Status:** actively maintained, macOS and Linux. Current releases: `am-i-hacked` 2.1.0, `am-i-being-recorded` 1.0.0, `secure-semgrep` 1.0.1. Each package's changelog has the detail. Website: <https://isaacbell.github.io/secure-devtools/>, with [guides](https://isaacbell.github.io/secure-devtools/guides/) such as [how to inspect an untrusted repository](https://isaacbell.github.io/secure-devtools/guides/inspect-untrusted-repository/) and [how to check AI-generated code](https://isaacbell.github.io/secure-devtools/guides/ai-generated-code-security/).

## Tools

Expand Down
8 changes: 4 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

This is a plan, not a promise. Items can change, slip or be dropped. There are no dates: a release ships when it is ready. Anything that is neither listed here nor filed as an issue is not scheduled.

Current release: **2.0.1**.
Current release: **2.1.0**.

## How releases work

- Every fix that merges ships as the next patch release: 2.0.2, then 2.0.3, and so on.
- Issues labeled `release-blocker` gate the next minor release. 2.1.0 ships once every issue with that label is closed.
- Every fix that merges ships as the next patch release: 2.1.1, then 2.1.2, and so on.
- Issues labeled `release-blocker` gate the next minor release. 2.2.0 ships once every issue with that label is closed.

## 2.1.0 candidates
## 2.2.0 candidates

A minor release that widens what the scanner reads and adds an opt-in feature.

Expand Down
31 changes: 11 additions & 20 deletions apps/am-i-hacked/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,38 @@

Newest first. Default-behavior changes are marked **CHANGED**.

## Unreleased
## 2.1.0 - 2026-10-09

**BLUF:** The system scan now checks the fixed places tooling installs code outside any project, the folder scan verifies Python virtualenv integrity, and the findings cap rises to 1000. `bin/host-audit.sh` is renamed to `bin/system-scan.sh`, with the old name kept as a shim.
**BLUF:** A security release with new checks. The scan no longer trusts the tree it is reviewing: a scanned repo's `.git/config` could make `am-i-hacked` or `safe-pull` run a command, and payloads in dot-directories, in folders named in `.ignore` or `.rgignore`, in tracked files matched by `.gitignore`, and in tracked build output were not read. It also adds a dark-corner system scan and Python virtualenv integrity, and raises the findings cap to 1000. `bin/host-audit.sh` is renamed to `bin/system-scan.sh`, with the old name kept as a shim. A 2.0.1 was prepared but never published; everything in it is in this release.

### Added
- The system scan (`--system`) now also scans a fixed list of "dark corners" where tooling installs code outside any project: Python virtualenvs, the uv and pip caches, pyenv, pipx, the npx cache, the pnpm global store, cargo, bun, deno, and the go and gem bin folders. Each is scanned with the folder scan when present and skipped silently when it does not exist.
- The system scan (`--system`) now also scans a fixed list of "dark corners" where tooling installs code outside any project: Python virtualenvs (`~/.venv` and `~/.virtualenvs`), pyenv versions, uv tools and cache, the pip cache, pipx, the npx cache, the pnpm global store, cargo's `bin`, bun, deno, go's `bin`, and the gem folder (`~/.gem`). Each is scanned with the folder scan when present and skipped silently when it does not exist.
- Python virtualenv integrity check in the folder scan, for every venv under the scanned folder: files that no longer match their package `RECORD` hashes are a `HIGH` finding; `RECORD` paths that leave the venv or pass through a symlinked folder (never opened), files and symlinks no package owns, console scripts and symlinks in `bin/` no package claims (each venv checked on its own claims), a console script name containing `|`, `RECORD` entries with no hash, a venv whose `RECORD` files or whole `dist-info` folders were deleted, a `_virtualenv.pth` that is not the stock one-line file, `.pth` and `sitecustomize` hooks that run code, unreadable files and folders (reported rather than skipped), and file names containing newlines are all reported.
- `--max-findings N` sets the maximum findings printed, from 1 to 999999999, default raised from 100 to 1000. **CHANGED:** when findings exceed the cap, the true total and a per-rule count are printed, and higher-severity findings are kept first.
- The system scan reports a check it could not run instead of passing it: `ps` missing or failing, a `crontab -l` error other than "no crontab", `codesign` missing, an AI-tool config `jq` cannot parse (JSONC), an unreadable dark corner, your own relative-path process whose working directory `lsof` could not resolve, and a login item's file that you own but cannot read (a root-owned one stays informational).

### Changed
- **CHANGED:** hex and unicode escape findings inside installed dependency folders (`site-packages`, `dist-packages`, `node_modules`) are reported as one summary count instead of one finding per match. `vendor/` is committed with the project, so its findings are still listed.
- **CHANGED:** `bin/host-audit.sh` is now `bin/system-scan.sh`. `bin/host-audit.sh` remains as a shim, and the `host` command still works.
- **CHANGED:** bundled code (webpack/esbuild/ncc) identified by markers in the first 4096 bytes (`__webpack_require__`, `__nccwpck_require__`, `webpackBootstrap`, `__toESM(`, `__commonJS(`) receives only high-signal checks. Routine patterns in bundles (dynamic code execution, network modules, child process) are counted but not flagged; obfuscation, capture/exfiltration and bot tokens still are.
- **CHANGED:** embedded WebAssembly data: URLs (`data:application/(wasm|octet-stream);base64,AGFzbQ...`) on a single line are no longer flagged for line length, as they are expected to be long.

### Security
- **CHANGED:** the scan now reads dot-directories, no longer reads `.ignore` or `.rgignore`, and scans tracked files that `.gitignore` matches. Committed build output is now scanned, so findings may appear in `dist/`, `.github/`, `.vscode/` and similar. Untracked build output listed in `.gitignore` is still skipped, and `node_modules` and `.git` are always skipped.
- Payloads were not scanned when they sat in a dot-directory, in a folder named in `.ignore` or `.rgignore`, in a tracked file matched by `.gitignore`, or in a tracked build output folder. All of these are scanned now.
- A scanned repo's `.git/config` could make the scan run a command: `git ls-files` runs any `core.fsmonitor` hook the config names. The scan now runs git with `core.fsmonitor=false`.
- `safe-pull` had the same flaw: its `git status` ran the `core.fsmonitor` command named in the working tree's `.git/config`. Every git call in `safe-pull` now runs with `core.fsmonitor=false`. Config that `git fetch` itself reads, such as `core.sshCommand`, is not covered, so run `safe-pull` only in a repository whose `.git` you created.
- **CHANGED:** 71 official Yarn releases in `.yarn/releases` are verified by SHA256 checksum; a mismatch is a finding at line 1. Unverified or malicious Yarn files no longer pass silently.
- A source file containing a NUL byte was skipped as binary, so one NUL in a comment hid the whole file from the folder scan. Source files are now always read as text.
- A systemd or autostart `Exec` line containing `*` was glob-expanded against the scanner's working directory. It is now split without globbing.
- A hook command containing a tab was cut at the tab, so a dangerous path after it was not checked.
- A persistence script whose file name contains a newline was not read.

### Fixed
- The `ignored null byte in input` warnings are no longer emitted, including for matched lines and build-output detection.

## 2.0.1 - 2026-10-01

**BLUF:** A security release. The scan no longer trusts the tree it is reviewing: a scanned repo's `.git/config` could make it run a command, and payloads in dot-directories, in folders named in `.ignore` or `.rgignore`, in tracked files matched by `.gitignore`, and in tracked build output were not read. It also fixes a skip on extensionless scripts.

### Security
- **CHANGED:** the scan now reads dot-directories, no longer reads `.ignore` or `.rgignore`, and scans tracked files that `.gitignore` matches. Committed build output is now scanned, so findings may appear in `dist/`, `.github/`, `.vscode/` and similar. Untracked build output listed in `.gitignore` is still skipped, and `node_modules` and `.git` are always skipped.
- Payloads were not scanned when they sat in a dot-directory, in a folder named in `.ignore` or `.rgignore`, in a tracked file matched by `.gitignore`, or in a tracked build output folder. All of these are scanned now.
- A scanned repo's `.git/config` could make the scan run a command: `git ls-files` runs any `core.fsmonitor` hook the config names. The scan now runs git with `core.fsmonitor=false`.
- **CHANGED:** 71 official Yarn releases in `.yarn/releases` are verified by SHA256 checksum; a mismatch is a finding at line 1. Unverified or malicious Yarn files no longer pass silently.

### Changed
- **CHANGED:** bundled code (webpack/esbuild/ncc) identified by markers in the first 4096 bytes (`__webpack_require__`, `__nccwpck_require__`, `webpackBootstrap`, `__toESM(`, `__commonJS(`) receives only high-signal checks. Routine patterns in bundles (dynamic code execution, network modules, child process) are counted but not flagged; obfuscation, capture/exfiltration and bot tokens still are.
- **CHANGED:** embedded WebAssembly data: URLs (`data:application/(wasm|octet-stream);base64,AGFzbQ...`) on a single line are no longer flagged for line length, as they are expected to be long.

### Fixed
- An extensionless script inside a folder with a dot in its name (for example `.devcontainer/sync-agent`) was skipped. The extensionless check now tests the file name.

Upgrade: `pnpm add -D am-i-hacked@2.0.1`, or run the pinned version without installing: `pnpx am-i-hacked@2.0.1 .`
Upgrade: `npm install --save-dev am-i-hacked@2.1.0`, or run the pinned version without installing: `npx am-i-hacked@2.1.0`

## 2.0.0 - 2026-10-01

Expand Down
2 changes: 1 addition & 1 deletion apps/am-i-hacked/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
npx am-i-hacked
```

**Status:** 2.0.1, macOS and Linux. The next release adds a dark-corner system scan, Python virtualenv integrity and `--max-findings`; the tables below document that next-release state, and [CHANGELOG.md](CHANGELOG.md) marks it Unreleased. Formerly published as `am-i-compromised`; the old command name still works.
**Status:** 2.1.0, macOS and Linux. Formerly published as `am-i-compromised`; the old command name still works.

## Install and run

Expand Down
2 changes: 1 addition & 1 deletion apps/am-i-hacked/bin/ioc-patterns.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ readonly IOC_CHILD_PROCESS_OPTIONS_OBJECT_PATTERN=',[[:space:]]*\{'
# key is quoted in JSON, so a closing quote may sit between the key and the
# colon: `"task.allowAutomaticTasks": true`.
#
# The MCP rule flags download-and-run shapes only; `uvx`, `pnpm dlx`, and `node`
# The MCP rule flags download-and-run shapes only; `uvx`, `pnpx`, and `node`
# servers are legitimate and must not be flagged.

readonly IOC_EDITOR_PATTERNS=(
Expand Down
9 changes: 9 additions & 0 deletions apps/am-i-hacked/bin/safe-pull.sh
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ if ! command -v git >/dev/null 2>&1; then
exit 2
fi

# Every git call below runs with the working tree's config defanged. A repository
# received as an archive carries its own `.git/config`, and `git status` runs any
# command that config names in `core.fsmonitor`. This does not cover config that
# `git fetch` itself reads (for example `core.sshCommand`): run safe-pull only in a
# repository whose `.git` you created, not one that arrived as an archive.
git() {
command git -c core.fsmonitor=false "$@"
}

if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
printf 'safe-pull: not inside a git working tree\n' >&2
exit 2
Expand Down
2 changes: 1 addition & 1 deletion apps/am-i-hacked/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "am-i-hacked",
"version": "2.0.1",
"version": "2.1.0",
"description": "Scan a project for malicious code and audit your machine for persistence, capture tools and risky AI-tool config",
"license": "MIT",
"author": "Isaac Bell <[email protected]>",
Expand Down
16 changes: 16 additions & 0 deletions apps/am-i-hacked/test/safe-pull.bats
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,22 @@ pull_guard() {
[ ! -f "$WORK/feature.js" ]
}

@test "core.fsmonitor from the working tree's .git/config does not run" {
printf 'console.log("feature")\n' >"$OTHER/feature.js"
commit_and_push "$OTHER" "add feature"

cd "$WORK"
# Set after setup, which would run the hook itself. The marker sits outside
# the working tree so it cannot dirty it.
git -C "$WORK" config core.fsmonitor "touch '$TMP/ran'; false"
pull_guard --dry-run
if [[ -e "$TMP/ran" ]]; then
fail "core.fsmonitor from the working tree ran during safe-pull"
fi
assert_success
assert_output --partial "clean (dry run)"
}

# -------------------------------------------------------------------------------
# Refusals
# -------------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion apps/am-i-hacked/test/scanner.bats
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ write_file() {

@test "editor MCP config: local package-runner servers are not flagged" {
write_file ".vscode/mcp.json" \
'{"servers":{"serena":{"type":"stdio","command":"uvx","args":["--from","git+https://github.com/oraios/serena","serena","start-mcp-server"]},"context7":{"type":"stdio","command":"pnpm","args":["dlx","@upstash/context7-mcp"]}}}'
'{"servers":{"docs-server":{"type":"stdio","command":"uvx","args":["--from","git+https://github.com/example/docs-server","docs-server","start"]},"context7":{"type":"stdio","command":"pnpm","args":["dlx","@upstash/context7-mcp"]}}}'
scan
assert_success
}
Expand Down
12 changes: 0 additions & 12 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,3 @@ at the new name:
```bash
pnpm deprecate am-i-compromised "Renamed to am-i-hacked. Run: npx am-i-hacked"
```

## Public repository

The public repository is staged from an allowlist, `scripts/public-paths.txt`, as one fresh
commit:

```bash
mise run export-public "$(mktemp -d)"
```

The export copies only listed paths, removes `!` lines, refuses local files, and fails on any
personal-data pattern hit.
4 changes: 2 additions & 2 deletions docs/guides/audit-developer-machine/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
<article class="wrap article">
<header class="page-head">
<h1>How to check whether your developer machine is compromised</h1>
<p class="byline muted">By <a href="https://isaacbell.io">Isaac Bell</a> · Updated <time datetime="2026-10-07">7 October 2026</time> · Applies to am-i-hacked 2.0.1</p>
<p class="byline muted">By <a href="https://isaacbell.io">Isaac Bell</a> · Updated <time datetime="2026-10-07">7 October 2026</time> · Applies to am-i-hacked 2.1.0</p>
</header>
<div class="answer"><p>Run <code>npx am-i-hacked --system</code>. It is a read-only audit that needs no root or sudo and makes no network calls. It checks login persistence (launchd on macOS, systemd user units and autostart on Linux) and the code signatures of what they launch, the crontab, shell startup files, AI coding tool config, and running processes. Pair it with <code>npx am-i-being-recorded</code> for screen-capture capability. Neither is antivirus: a clean result means no warning signs were found.</p></div>
<div class="prose">
Expand All @@ -121,7 +121,7 @@ <h2 id="what">What the system scan checks</h2>
<tr><td>Shell startup files</td><td>Remote scripts piped to a shell, decoded payloads, <code>DYLD_INSERT_LIBRARIES</code> or <code>LD_PRELOAD</code>, TLS checks disabled, a forced <code>NODE_OPTIONS --require</code>, <code>sudo</code> or <code>ssh</code> replaced by an alias, a redirected AI API base URL, a proxy or extra certificate authority</td></tr>
<tr><td>AI coding tool config</td><td>User-level and managed settings for Claude Code, Codex, Cursor, Gemini CLI, OpenCode and Kilo Code: permission bypass, redirected API URLs, plain-text keys, MCP servers that run unpinned code, and hooks that run from writable places or observe your prompts</td></tr>
<tr><td>Running processes</td><td>Interpreters running capture scripts or scripts from staging areas such as Downloads, caches and <code>/tmp</code></td></tr>
<tr><td>Dark corners (next release)</td><td>Folders where tooling installs code outside any project: virtualenvs, pip and uv caches, pyenv, pipx, the npx cache, the pnpm store, cargo, bun, deno, go and gem bin folders</td></tr>
<tr><td>Dark corners</td><td>Folders where tooling installs code outside any project: virtualenvs, pip and uv caches, pyenv, pipx, the npx cache, the pnpm store, cargo, bun, deno, go and gem bin folders</td></tr>
</tbody>
</table></div>

Expand Down
Loading
Loading