🚧Work In Progress🚧
Cross-platform supply-chain guard for every package manager on your machine. Silently blocks too-young versions, known-malicious packages and unsigned publishes — across npm, cargo, pypi, nuget — without touching your build tools.
# Three commands, once.
$ sakimori proxy install-ca # trust the proxy's root CA
$ sakimori proxy install-daemon # auto-run in the background
$ sakimori install-gate install # route your shell through it
# Business as usual, permanently safer.
$ npm install react
# → proxy silently drops versions < 7d old
# → npm picks the newest older version
# → no error, no broken build, just a measurably safer dependencyWorks on macOS, Linux, and Windows. Also ships a CI mode
(deps check + eBPF/ETW supervisor) for pipelines.
- Why this exists
- How it works — proxy architecture, 4 ecosystems
- Install
- Desktop quick start
- Feature reference — every subcommand with examples
- CI usage (GitHub Actions)
- Docker image
- Configuration reference
- Troubleshooting
- Known limitations — what this honestly can't do
- Development
Supply-chain attacks follow a predictable timeline:
- Attacker publishes a malicious version at
T+0 - Community notices, yanks it at
T+12–72h
Most victims install between hours 0–12. pnpm 10.x introduced
minimumReleaseAge
to solve this for npm only — versions younger than the threshold
become invisible to the resolver, which silently falls back to the
newest older one.
sakimori brings the same behaviour to all four major ecosystems (crates.io, npm, pypi, nuget) and any package manager that talks to them, by sitting as an HTTPS proxy and rewriting the registry's metadata responses in-flight. No resolver integration. No config in your package manifests.
┌───────────────────┐ ┌────────────────────┐
│ npm / cargo / │ │ │
user ───► │ pip / uv / │ ─────►│ sakimori proxy │ ──► real registry
│ dotnet / poetry │ HTTPS│ (localhost:8910) │ (metadata + tarball)
└───────────────────┘ └─────────┬──────────┘
│
▼
rewrites metadata:
- drop versions < --min-age
- drop unsigned versions (--require-provenance)
- retarget npm dist-tags.latest
- returns 403 for pinned tarball fetches
to too-young versions
The proxy's root CA is installed into the system trust store once;
from then on, every HTTPS request your package managers make through
HTTPS_PROXY=http://127.0.0.1:8910 gets transparently filtered.
| ecosystem | silent auto-fallback | hard deny on pinned fetch |
|---|---|---|
| crates.io | ✅ sparse-index JSONL rewrite (drops too-young lines from /<prefix>/<name>) |
✅ 403 on .crate download to a denied version |
| npm | ✅ packument rewrite (drops versions + retargets dist-tags.latest) |
✅ 403 on .tgz download |
| pypi | ✅ Warehouse JSON API (/pypi/<pkg>/json) + PEP 691 Simple JSON + PEP 503 Simple HTML via JSON-API lookup |
✅ 403 on files.pythonhosted.org tarball download |
| nuget | ✅ registration-page rewrite (/v3/registration*/...) + flat-container index via registration lookup |
✅ 403 on .nupkg download |
| vscode-marketplace | ✅ extensionquery JSON rewrite (drops versions[].lastUpdated younger than --min-age) on marketplace.visualstudio.com + open-vsx.org |
⏳ .vsix lifecycle gate planned (roadmap #21) |
All five ecosystems' metadata paths now rewrite silently — pnpm-style
minimumReleaseAge across the board, no fail-hard in the common case.
sakimori has two layers, and they have different OS coverage. Read this carefully before assuming "macOS isn't supported" or "Linux gets everything":
| capability | Linux | macOS | Windows |
|---|---|---|---|
| Fetch-layer (proxy + install-gate) | |||
↳ sakimori proxy start (MITM + age filter + auto-fallback) |
✅ | ✅ | ✅ |
↳ sakimori install-gate install (shell wiring) |
✅ zsh / bash / fish | ✅ zsh / bash / fish | ✅ PowerShell |
↳ ~/.sakimori/installs.jsonl recording — who installed what, when |
✅ | ✅ | ✅ |
↳ sakimori advisories scan (OSV JOIN over the install log) |
✅ | ✅ | ✅ |
| ↳ Lifecycle-script inspection (`--lifecycle-policy audit | block`) | ✅ | ✅ |
↳ sakimori deps check / verify-cache / watch |
✅ | ✅ | ✅ |
Supervisor-layer (sakimori run / daemon) |
|||
| ↳ exec / open / connect events | ✅ eBPF | ❌ planned (roadmap 5b) | ✅ ETW |
| ↳ PPid attribution → package-manager origin | ✅ | ❌ | partial |
↳ --snapshot-workspace (drift + known-IOC scan at shutdown) |
✅ | ❌ | partial |
| ↳ Live network block | ✅ eBPF cgroup hooks | ❌ planned (#5) | ✅ Defender Firewall |
| ↳ Live file/exec block | tripwire (SIGKILL); pre-syscall in progress (#4) | ❌ | audit-only |
Headline: if you only care about "tell me which packages I installed and warn me when one of them gets a CVE next week", macOS is a first-class platform. That's the part most users want. The supervisor (live blocking, exec attribution, workspace drift) is where the Mac gap is — tracked as roadmap item 5b in CLAUDE.md (Apple's Endpoint Security framework, requires Apple-issued entitlement + SystemExtension signing).
CI coverage matches: the macos-smoke workflow
exercises proxy start → pinned-tarball fetch → installs.jsonl →
advisories scan → install-gate shellenv end-to-end on every PR
that touches the relevant crates, so the cells marked ✅ for macOS
above don't silently regress.
The 2026-05 GitHub-internal-repo compromise (poisoned VS Code extension on an employee device) made it concrete: editor and browser extensions are a parallel distribution channel that almost nothing on the supply-chain market actually catches end-to-end. sakimori covers it across four layers, each addressing a failure mode the others can't:
| layer | what catches | where in sakimori |
|---|---|---|
| Fetch (proxy) | Marketplace install of a young / freshly-published extension | extensionquery JSON rewriter on marketplace.visualstudio.com + open-vsx.org — silent minimumReleaseAge-style fallback per ecosystem #20 |
| Runtime attribution | Extension subtree opens ~/.ssh/id_ed25519 / hits IMDS / executes a downloaded payload |
PPid walker recognises code / cursor / windsurf / code-server / Code Helper (Plugin) and stamps source: vscode on every Connect / Open / Exec event the extension subtree produces — persistence-write, cloud-secret-egress, IOC scanner all fire #19 |
| Workspace poisoning | A repo with .vscode/tasks.json auto-running on folderOpen |
IOC catalog v2026.05.21+ ships vscode.tasks-folderopen-autorun as a basename-scoped content needle (High severity, family editor-extension) #23 |
| Sideload tamper | An extension installed bypassing the Marketplace fetch (e.g. dragged-in .vsix, manual git clone into ~/.vscode/extensions/) |
sakimori extensions snapshot / extensions diff auto-discovers ~/.vscode, ~/.vscode-insiders, ~/.cursor, ~/.windsurf, plus the platform globalStorage tree; the diff runs the IOC catalog against every added / modified path #24 |
A few existing tools in this space try to be a registry mirror
for the VS Code Marketplace — they stand up a server, scrape
Microsoft's gallery, and ask users to repoint VS Code at the
mirror via product.json edits or extensions.gallery.serviceUrl
overrides. That approach has real friction:
- VS Code has no first-class "alternate registry" config. The
marketplace URL is hardcoded in
product.json; switching it requires modifying Microsoft's binary, which the EULA forbids redistributing. (VSCodium, Cursor, Code-OSS, Windsurf — forks — do expose a configurable gallery setting; the EULA issue is specific to upstream VS Code.) - Marketplace ToS treats redistribution carefully. §3 of the VS Code Marketplace terms allows access to the gallery for downloading extensions; standing up an independent mirror that re-serves the gallery to other users sits in genuinely murky territory. Several mirror-style projects have hit takedown notices or quietly become enterprise-only because of this.
sakimori takes a different shape — an endpoint MITM proxy, not a mirror. That sidesteps both problems:
- No editor binary modification. The user sets
HTTPS_PROXY=http://127.0.0.1:8080(viasakimori install-gate install, the same one-linernpm installalready uses) and trusts sakimori's local CA. The marketplace request the editor makes is unchanged; only the response the editor receives is filtered to drop too-young versions.product.jsonstays byte-for-byte identical to whatever Microsoft shipped. - No re-serving. sakimori never stands up an authoritative gallery. It MITMs the user's own request to Microsoft (or Eclipse for OpenVSX), filters the JSON in transit, and hands it back. The bytes leave sakimori the moment the editor reads them; no per-tenant caching or republication. Architecturally this is the same posture Bitdefender, Kaspersky, Cisco Umbrella, and corporate SSL-inspection appliances have run for a decade — well-understood, both legally and operationally.
- Same proxy, every editor. One running instance covers VS
Code, Cursor, VSCodium, Code-OSS, code-server, and any other
editor that speaks the Marketplace / OpenVSX
extensionqueryAPI. No per-editor configuration knob.
The proxy alone doesn't solve the problem — a determined attacker
ships an extension with a deliberate publication delay so it
clears --min-age, or sideloads via .vsix to bypass the proxy
entirely. That's why the four-layer table above matters:
- A sideloaded extension never hits the proxy →
extensions diffstill catches it (new files under~/.vscode/extensions/). - An aged-into-marketplace extension passes the rewriter →
attribution + persistence-write rule pack catches the actual
malicious behaviour (writing to
~/.ssh/, hitting IMDS, etc.) at runtime. - A workspace
.vscode/tasks.jsonautorun never touches the marketplace at all → the IOC catalog catches the dropper primitive directly.
Registry-firewall tools (Sonatype Nexus Firewall, JFrog Xray) sit at the right layer for #1 but only for the proxy path. EDR / SCA tools cover none of the four directly. sakimori is the only endpoint agent we're aware of that covers all four with a shared attribution + IOC backbone.
.vsix / .crx lifecycle gate (block on activationEvents: ["*"]-style autorun primitives at fetch time), Ecosystem:: VscodeExtension propagation into the install log and OSV-JOIN
advisory scan, and Chrome Web Store coverage are all tracked in
CLAUDE.md roadmap entries #20–#24. Pull requests
welcome.
Pick whichever fits your setup.
brew install bokuweb/sakimori/sakimori
# ↑ the repo-is-its-own-tap convention; no separate `brew tap` needed.Auto-updated on every release via the homebrew-formula.yml
workflow — the formula lives at
HomebrewFormula/sakimori.rb
in this repo.
# macOS (Apple Silicon)
curl -fsSL https://github.com/bokuweb/sakimori/releases/latest/download/sakimori-aarch64-apple-darwin.tar.gz \
| sudo tar -xz -C /usr/local/bin
# macOS (Intel)
curl -fsSL https://github.com/bokuweb/sakimori/releases/latest/download/sakimori-x86_64-apple-darwin.tar.gz \
| sudo tar -xz -C /usr/local/bin
# Linux (x86_64 musl static)
curl -fsSL https://github.com/bokuweb/sakimori/releases/latest/download/sakimori-x86_64-unknown-linux-musl.tar.gz \
| sudo tar -xz -C /usr/local/bin
# Windows (PowerShell)
Invoke-WebRequest -Uri https://github.com/bokuweb/sakimori/releases/latest/download/sakimori-x86_64-pc-windows-msvc.tar.gz -OutFile c.tgz
tar -xzf c.tgz -C "$env:USERPROFILE\.local\bin"Every release also ships a .sha256 sidecar. The archive contains
the sakimori binary (Linux also ships sakimori.bpf.o for the
supervised-run mode).
docker run --rm -p 8910:8910 \
-v sakimori-conf:/etc/sakimori-xdg \
ghcr.io/bokuweb/sakimori-proxy:v0 \
--listen 0.0.0.0:8910 --min-age 7dMount /etc/sakimori-xdg as a volume to persist the generated
root CA across container restarts. See Docker image.
cargo install --git https://github.com/bokuweb/sakimori sakimoriThe Linux eBPF supervised-run mode additionally needs
rustup toolchain install nightly --component rust-src +
cargo install bpf-linker. Not required for proxy / deps / install-gate.
Three commands, once per machine. Each is idempotent.
# 1. Generate the proxy's root CA and install it into the system
# trust store. macOS uses `security`, Linux uses
# `update-ca-certificates`, Windows uses elevated
# `Import-Certificate` (triggers one UAC prompt).
sakimori proxy install-ca
# 2. Register the proxy as a background service so it's always up.
# macOS: ~/Library/LaunchAgents/com.sakimori.proxy.plist
# Linux: ~/.config/systemd/user/sakimori-proxy.service
# Windows: Task Scheduler /sakimori-proxy
sakimori proxy install-daemon
# Follow the printed `launchctl bootstrap …` / `systemctl --user enable --now`
# / `schtasks.exe /Create …` line.
# 3. Append HTTPS_PROXY + CA bundle env vars to your shell rc.
# Detects zsh / bash / fish / PowerShell from $SHELL (or your OS).
sakimori install-gate installOpen a new shell — everything's wired:
$ env | grep -E 'HTTPS_PROXY|CARGO_HTTP_CAINFO'
HTTPS_PROXY=http://127.0.0.1:8910
CARGO_HTTP_CAINFO=/Users/you/.config/sakimori/ca.pem
$ sakimori doctor
sakimori doctor
────────────────────────────────────────────────────────────
✓ CA certificate /Users/you/.config/sakimori/ca.pem (644 bytes)
✓ CA private key /Users/you/.config/sakimori/ca.key
✓ Proxy reachable accepted TCP on 127.0.0.1:8910
✓ $HTTPS_PROXY http://127.0.0.1:8910
✓ install-gate rc /Users/you/.zshrc
✓ Daemon unit /Users/you/Library/LaunchAgents/com.sakimori.proxy.plist
────────────────────────────────────────────────────────────
6 check(s): 0 fail, 0 warn
From here, npm install / pnpm add / yarn add / cargo add /
cargo build / pip install / uv add / poetry add /
dotnet add package / dotnet restore all flow through the proxy.
$ curl -s https://index.crates.io/se/rd/serde | wc -l # direct
315
$ curl -sx http://127.0.0.1:8910 https://index.crates.io/se/rd/serde | wc -l
306 # the 9 most recent versions are invisible to cargo's resolvercargo picks the newest remaining in-range version — no error, just safer. Same shape on the other three ecosystems.
Reverse each step (same flags):
sakimori install-gate uninstall # strip block from shell rc
sakimori proxy uninstall-daemon # remove launchd / systemd / Task Scheduler unit
sakimori proxy uninstall-ca # remove CA from system trust store
rm -rf ~/.config/sakimori # delete CA + key (optional)Start the MITM HTTPS proxy in the foreground. install-daemon
wraps this for background use; run it directly when you want logs
on stdout or you're running the proxy yourself in Docker.
Run sakimori proxy start --help for the canonical, always-up-to-date
flag list. The grouping below summarises the surface so you know
what knobs exist; defaults are tuned for "drop into ~/.zshrc and
forget" desktop use — CI workflows usually want to layer on the
lifecycle gate and provenance check.
sakimori proxy start [OPTIONS]
| group | flags | what it does |
|---|---|---|
| Networking | --listen <ADDR> (default 127.0.0.1:8910), --config-dir <PATH> |
Where the proxy listens; where its CA / config files live. |
| Release-age gate | --min-age <DURATION> (default 7d), --fail-on-missing |
Versions younger than --min-age are silently dropped from the metadata response the client sees. --fail-on-missing treats unknown publish dates as deny (default: fail-open). |
| Provenance gate (npm only) | --require-provenance |
Drop every npm version that doesn't carry a Sigstore provenance claim. Closes the "stolen publish token" hole --min-age alone can't cover — a thief can publish immediately, but without an OIDC-authenticated CI run can't attach valid provenance. |
| Known-malicious gate | --osv, --osv-mirror, --osv-mirror-url <URL> |
Consult OSV.dev (live) and/or the sakimori-hosted pre-filtered mirror; hard-deny versions tagged MAL-* / known-malicious regardless of --min-age. |
| Typosquat detection | --typosquat {warn,block}, --typosquat-mirror, --typosquat-mirror-url <URL> |
Compare incoming package names against a top-N-per-ecosystem list (lodash, requests, tokio, Newtonsoft.Json, …) and warn or block close-distance candidates. |
| Lifecycle-script gate (Shai-Hulud-class defence) | --lifecycle-policy {audit,block,strip}, --lifecycle-allow <PKG> (repeatable), --lifecycle-strip-on-failure {block,passthrough}, --lifecycle-strip-cache-dir <DIR>, --lifecycle-no-strip-cache |
audit logs install-time scripts; block 403s tarballs that ship them; strip rewrites the tarball in place to drop the script keys + recompute the SRI hash + amend the packument so npm's integrity verifier agrees. See CLAUDE.md Roadmap #15 for the threat model. |
| Egress allow-list | --network-allow <HOST> (repeatable), --network-allow-file <PATH> |
Default-deny hostname filter. Patterns: host.example.com (exact) or *.example.com (any subdomain, excludes apex). Off by default. |
| Install log + advisories | --no-install-log, --install-log <PATH> |
The local-first append-only audit log feeding sakimori advisories scan. On by default at ~/.sakimori/installs.jsonl. |
| OTLP fan-out | --otlp-endpoint <URL>, --otlp-header <K=V> (repeatable) |
Mirror every allowed install as an OTLP/HTTP LogRecord to Datadog / Honeycomb / Loki / a self-run otel-collector. The wire envelope is spec-compliant OTLP/HTTP JSON (any collector parses it); the package.* attribute keys are sakimori-specific, not OpenTelemetry semantic conventions — OTel has no "package install" semconv yet. See OTLP semantic conventions below. |
| Custom registries | --npm-registry, --pypi-registry, --pypi-files-host, --cargo-registry-host, --cargo-sparse-host, --nuget-registry (all repeatable), --registries-config <FILE>, --upstream-ca-file <PATH> (repeatable) |
Teach the proxy about internal mirrors / replacement registries so the rewriters + lifecycle gate fire on their traffic too. --upstream-ca-file adds a PEM CA to the upstream rustls trust store for mirrors behind a private CA. See the Custom registries subsection below. |
First-run side effect: generates a self-signed root CA at the config dir and prints the OS-specific trust command. Subsequent runs reuse the existing CA.
Egress allow-list closes the eBPF-by-IP gap: when you also run
sakimori run with a network policy, the kernel layer enforces by
resolved IP and loses against CDN rotation. The proxy's hostname
filter sees the SNI / Host: value the client actually asked for,
so an entry like *.githubusercontent.com matches every rotating
CDN IP automatically — the same convention step-security/harden-runner
users are used to:
sakimori proxy start \
--network-allow api.github.com \
--network-allow '*.githubusercontent.com' \
--network-allow registry.npmjs.orgThe rewriters + lifecycle gate dispatch by hostname. By default
only the canonical public hosts are watched — traffic to
registry.npmjs.org runs the npm packument rewriter, traffic to
pypi.org runs the PyPI rewriters, etc. Internal mirrors /
replacement registries (Verdaccio, GitHub Packages, Artifactory,
Takumi Guard, JFrog, Nexus, …) are passed through opaquely unless
you teach the proxy about them.
Three layered sources, applied in order with case-insensitive
dedupe: built-in defaults → optional TOML config file
(--registries-config <FILE>) → per-ecosystem CLI flags. The
canonical public hosts remain watched alongside your additions.
# CLI flags (repeatable). Each accepts a bare hostname or a URL —
# the host part is extracted (https://npm.flatt.tech:8443/path →
# npm.flatt.tech).
sakimori proxy start \
--npm-registry npm.flatt.tech \
--npm-registry 'https://npm.corp.internal:8443/' \
--pypi-registry pypi.corp.internal \
--nuget-registry nuget.corp.internal| flag | feeds | canonical default |
|---|---|---|
--npm-registry |
npm packument + tarball | registry.npmjs.org |
--pypi-registry |
PyPI Warehouse JSON + Simple index | pypi.org |
--pypi-files-host |
PyPI sdist + wheel downloads | files.pythonhosted.org |
--cargo-registry-host |
crates.io API /api/v1/crates/… |
crates.io |
--cargo-sparse-host |
crates.io sparse index | index.crates.io |
--nuget-registry |
NuGet registration + flat-container | api.nuget.org |
Or pin the same lists in a config file (so a team can ship one canonical config and not paste the same flags into every invocation):
# ~/.config/sakimori/registries.toml
[registries]
npm = ["registry.npmjs.org", "npm.flatt.tech"]
pypi_index = ["pypi.org"]
pypi_files = ["files.pythonhosted.org"]
crates = ["crates.io"]
crates_sparse = ["index.crates.io"]
nuget = ["api.nuget.org"]sakimori proxy start --registries-config ~/.config/sakimori/registries.tomlTo lock the proxy to only the internal mirrors and reject the
canonical public hosts, combine with --network-allow:
sakimori proxy start \
--registries-config /etc/sakimori/registries.toml \
--network-allow npm.corp.internal \
--network-allow pypi.corp.internalIf the internal mirror's TLS chain is signed by a private CA
not in the webpki-roots-shipped trust store, pass each CA PEM
file with --upstream-ca-file (repeatable). Without this the
upstream handshake fails with UnknownIssuer even when the
hostname is on --registries-config:
sakimori proxy start \
--registries-config /etc/sakimori/registries.toml \
--upstream-ca-file /etc/ssl/corp-root-ca.pem \
--upstream-ca-file /etc/ssl/intermediate.pemNon-goals (intentionally not done):
- Path-shape rewriting. The custom host must serve the canonical
registry's URL shape (npm packument +
/<pkg>/-/<pkg>-<ver>.tgz; PyPI Warehouse JSON / PEP 503/691 Simple; NuGet v3 registration + flat-container; cargo sparse). A mirror that exposes a different layout (e.g. Artifactory at/artifactory/api/npm/<repo>/) needs a path-prefix-aware parser variant — not implemented. dist.tarballURL rewriting. The npm rewriter preserves the upstream's own tarball URL byte-for-byte — mirrors that serve their own tarball URLs keep doing so transparently.
--otlp-endpoint emits one OTLP/HTTP JSON LogRecord per
allowed install. Two layers, with different compliance stories:
-
Envelope — OTLP-wire compliant. The
resourceLogs[].scopeLogs[].logRecords[]shape, the proto3→JSON name mapping (camelCase wire names;timeUnixNanoas a decimal string for 64-bit ints), theAnyValuevariant keys (stringValue,intValue, …), and the resource attributesservice.name/service.versionall follow the OTLP spec. Any spec-compliant collector (otel-collector-contrib, Datadog Agent's OTLP receiver, Honeycomb's OTel endpoint, Loki, …) parses the payload. This is enforced bycrates/sakimori-proxy/tests/otlp_proto_roundtrip.rs, which deserializes every emitted payload throughopentelemetry-proto's generatedExportLogsServiceRequesttype — same strict shape gate a real collector applies. -
Attribute keys — sakimori-specific, NOT semconv. The per-install fields use a
package.*namespace (package.ecosystem,package.name,package.version,package.resolved_at,package.execution_mode,package.project_path,package.user_agent,package.git.*). OpenTelemetry has no registered "package install event" attribute set yet, so we use our own namespace rather than shoehorn the data intocode.*orvcs.*. If/when semconv ships an official equivalent (e.g.software.package.*), sakimori will add it alongside the existing keys rather than rename — existing dashboards keep working.
If you grep your collector config for "semconv-compliant attributes": no, these aren't. If you grep for "OTLP-wire compatible": yes, they are.
Add / remove the root CA from the OS trust store. Cross-platform:
| OS | Mechanism | Privilege prompt |
|---|---|---|
| macOS | security add-trusted-cert -k /Library/Keychains/System.keychain |
sudo |
| Linux | copy to /usr/local/share/ca-certificates/ + update-ca-certificates |
sudo |
| Windows | Import-Certificate -CertStoreLocation Cert:\LocalMachine\Root |
UAC via Start-Process -Verb RunAs |
If you're not elevated, sakimori prints the exact shell command and exits — no silent reruns with privileges.
sakimori proxy install-ca [--config-dir <PATH>]
sakimori proxy uninstall-ca [--config-dir <PATH>]
Write a user-level service unit so the proxy runs in the background at login and restarts on failure.
| OS | Unit | Location |
|---|---|---|
| macOS | launchd plist (KeepAlive, RunAtLoad, Background ProcessType) |
~/Library/LaunchAgents/com.sakimori.proxy.plist |
| Linux | systemd --user unit (Restart=on-failure, WantedBy=default.target) |
~/.config/systemd/user/sakimori-proxy.service |
| Windows | Task Scheduler v1.4 XML (LogonTrigger, RestartOnFailure 99×1m, Hidden) |
%LOCALAPPDATA%\sakimori\sakimori-proxy.task.xml |
sakimori proxy install-daemon [OPTIONS]
Options:
--listen <ADDR> [default: 127.0.0.1:8910]
--min-age <DURATION> [default: 7d]
--binary <PATH> Override the sakimori binary path baked
into the unit. Defaults to the canonical
path of the currently-running executable.
The command prints the exact activation line (launchctl bootstrap
/ systemctl --user enable --now / schtasks.exe /Create) — run
that to start the service.
Edit the user's shell rc file so every new shell exports
HTTPS_PROXY + CA-bundle env vars pointing at the proxy. Idempotent
via # >>> sakimori install-gate >>> sentinels.
sakimori install-gate shellenv [--listen <ADDR>] [--shell {bash,zsh,fish,powershell}]
sakimori install-gate install [--rc <PATH>] [--shell ...]
sakimori install-gate uninstall [--rc <PATH>] [--shell ...]
Environment variables set (per shell):
| var | who uses it |
|---|---|
HTTPS_PROXY / HTTP_PROXY (+ lowercase variants) |
curl, npm, pip, cargo, dotnet, git |
CARGO_HTTP_CAINFO |
cargo (uses libcurl; doesn't honour system trust store on Linux) |
PIP_CERT |
pip |
NODE_EXTRA_CA_CERTS |
npm, yarn, pnpm |
REQUESTS_CA_BUNDLE |
Python requests, poetry, uv |
SSL_CERT_FILE |
generic OpenSSL-using tools |
Default rc file per shell:
| shell | path |
|---|---|
| bash | ~/.bashrc |
| zsh | ~/.zshrc |
| fish | ~/.config/fish/config.fish |
| powershell | $PROFILE = ~/Documents/PowerShell/Microsoft.PowerShell_profile.ps1 |
One-command diagnostic. Checks:
- CA certificate exists + non-empty
- CA private key exists +
chmod 600(Unix) - Proxy is accepting TCP on
--listen $HTTPS_PROXYin the current shell matches the proxy address- Shell rc file contains the install-gate sentinel
- Daemon unit file exists at the expected location
Exits 0 on no failures (warnings are informational), 1 otherwise.
sakimori doctor [--listen <ADDR>] [--config-dir <PATH>] [--rc <PATH>]
Sample output when the proxy is down:
✓ CA certificate /Users/you/.config/sakimori/ca.pem (644 bytes)
✓ CA private key /Users/you/.config/sakimori/ca.key
✗ Proxy reachable no listener on 127.0.0.1:8910: Connection refused
↳ start it: `sakimori proxy start` (or, for background: `sakimori proxy install-daemon`)
! $HTTPS_PROXY unset in this shell
↳ run `sakimori install-gate install` and open a new shell
Lockfile-level age gate, usable standalone (no proxy required). Good for a pre-install CI step that fails the build before the malicious package is even fetched.
sakimori deps check --min-age 7d Cargo.lock package-lock.json
# Different thresholds per ecosystem? Run twice.
sakimori deps check --min-age 14d Cargo.lock
sakimori deps check --min-age 3d package-lock.json
# Ignore first-party packages.
sakimori deps check --min-age 7d --ignore '@my-org/*' package-lock.json
# Machine-readable output for CI gating.
sakimori deps check --min-age 7d --format json Cargo.lockSupported lockfile formats:
| ecosystem | lockfile | registry endpoint consulted |
|---|---|---|
| cargo | Cargo.lock |
crates.io/api/v1/crates/<name> |
| npm | package-lock.json (lockfileVersion ≥ 2) |
registry.npmjs.org |
| pypi | uv.lock, poetry.lock, requirements.txt (exact == pins only) |
pypi.org/pypi/<name>/<version>/json |
| nuget | packages.lock.json (central-package-management) |
api.nuget.org/v3/registration5-{semver1,gz-semver2}/… |
Exit codes:
| code | meaning |
|---|---|
| 0 | all packages meet the threshold |
| 1 | at least one violation |
| 2 | parse or I/O error |
Cache location: $XDG_CACHE_HOME/sakimori/deps-cache.json
(%LOCALAPPDATA%\sakimori\… on Windows). Publish dates are
immutable, so there's no TTL.
Re-hash the package manager's local cache against the lockfile's
integrity: fields and fail if any byte doesn't match what the
lockfile pinned. This catches the content half of the TanStack
2025 npm supply-chain attack: a tarball restored from actions/ cache whose bytes have been swapped, while the lockfile entry
itself looks untouched.
Run it right after install, in the brief moment when the store is fully populated but nothing has built against it yet:
# npm cacache (uses ~/.npm/_cacache by default)
sakimori deps verify-cache --lockfile package-lock.json
# pnpm store v3 (auto-picks ~/.local/share/pnpm/store/v3 on Linux,
# ~/Library/pnpm/store/v3 on macOS)
sakimori deps verify-cache --lockfile pnpm-lock.yaml
# cargo registry cache (walks $CARGO_HOME/registry/cache/*/)
sakimori deps verify-cache --lockfile Cargo.lock
# Override the store path (monorepos with isolated stores, corporate
# runners with non-standard layouts). Windows defaults are auto-
# detected (`%LOCALAPPDATA%\npm-cache\_cacache`, `%LOCALAPPDATA%\pnpm\store\v3`).
sakimori deps verify-cache --lockfile pnpm-lock.yaml --cache /opt/pnpm-store/v3
# Machine-readable for CI gating
sakimori deps verify-cache --lockfile package-lock.json --format jsonSupported stores:
| ecosystem | lockfile | store walked |
|---|---|---|
| npm | package-lock.json (v2/v3) |
~/.npm/_cacache/content-v2/<algo>/<aa>/<bb>/<rest> |
| pnpm | pnpm-lock.yaml (v6–v9) |
<store>/v3/files/<aa>/<rest>[-exec] + per-tarball -index.json |
| cargo | Cargo.lock |
$CARGO_HOME/registry/cache/<reg>/<name>-<version>.crate |
Exit codes:
| code | meaning |
|---|---|
| 0 | every lockfile entry verifies cleanly against the store |
| 1 | at least one mismatch or missing-from-store entry |
| 2 | parse / I/O error |
⚠️ Honest limitations. The pnpm verifier reads the on-disk<rest>-index.jsonto find per-file hashes — pnpm discards the tarball after extraction, so a fully coordinated rewrite of both the index and every blob it references would verify clean. The realistic single-file tampering pattern is caught. pnpm v11+ (the next major after v10 — v10 itself still uses JSON) replaces the per-package JSON index with a single SQLiteindex.dbwhose BLOB values use msgpackr's non-standarduseRecords: trueextension. v11 stores are not yet supported andverify-cachewill surface a clearUnsupportederror rather than silently passing. Workaround until the reader lands: pin pnpm to<11.
The same check is wrapped as a one-line GitHub Actions step — see CI usage below.
Long-running FS-event watcher for lockfile changes. Designed for launchd at login.
# One-off (Ctrl-C to quit)
sakimori deps watch ~/code --min-age 7d
# With modal prompts (Keep / Revert via osascript)
sakimori deps watch ~/code --min-age 7d --action prompt
# Stdout logging, e.g. for tmux / screen
sakimori deps watch ~/code --min-age 7d --notifier stdout--action controls what happens on violation:
| value | behaviour |
|---|---|
notify (default) |
Desktop notification. Lockfile untouched, nothing blocked. |
prompt (macOS only) |
Keep / Revert modal via osascript. Revert runs git checkout HEAD -- <lockfile>. |
revert |
Silently restore the lockfile to HEAD via git. Destructive; file must be tracked. |
⚠️ Watch is detection, not prevention. FS events fire after the package manager finishes writing the lockfile — sopreinstall/install/postinstallscripts have already run. To actually prevent attacks, use the proxy (which sees every fetch) ordeps checkbefore install.
See packaging/macos/README.md for the launchd plist.
Detect unexpected file edits made during a build — the supply-chain
analogue of "did this npm install rewrite my source files /
.git/config / CI configuration?". Pure offline; no network.
# Before the build
coronarium workspace snapshot $GITHUB_WORKSPACE -o /tmp/before.json
cargo build # …or whatever you actually want to audit
# After the build — exits non-zero on any drift
coronarium workspace diff /tmp/before.json $GITHUB_WORKSPACEWhat the diff reports: files added, modified (size or SHA-256 changed), or removed between the two snapshots.
Always-skipped directory basenames (anywhere in the tree):
.git, node_modules, target, dist, build, vendor,
__pycache__, .venv, venv, .next, .turbo, .cache.
The list is hardcoded — .gitignore is not honoured because
an attacker can write into it. Pass --skip <name> (repeatable)
to extend the list for your own build artefacts.
Symlinks are recorded by target string; the link target is not
dereferenced. Files larger than 64 MiB default to a size-only
entry (no SHA), so two oversized files with identical sizes but
different contents will read as unchanged — bump
--max-file-bytes if that matters for your repo.
--format json for machine-readable output. --allow-drift
suppresses the non-zero exit when you only want the report.
The editor-extension counterpart of workspace snapshot. Auto-
discovers every editor extension root that exists on the host —
~/.vscode/extensions/, ~/.vscode-insiders/extensions/,
~/.cursor/extensions/, ~/.windsurf/extensions/, plus the
platform-appropriate VS Code User/globalStorage/ — and produces
one merged snapshot. Each file's relative path is prefixed with
the root's label (vscode-extensions/foo.bar-1.0.0/package.json)
so the same extension id installed under two editors doesn't
collide.
# Take a baseline (run when you trust the current state)
sakimori extensions snapshot -o ~/.sakimori/extensions-baseline.json
# Some time later — perhaps after `git pull`, perhaps daily via cron
sakimori extensions diff ~/.sakimori/extensions-baseline.jsonThe diff reports added / modified / removed entries and runs the
known-IOC catalog against every implicated path: a sideloaded
.vsix whose package.json references discord.com/api/ webhooks/, or a workspace's .vscode/tasks.json configured to
auto-run on folderOpen, surfaces as both a structural drift
entry and a High-severity IOC hit. High-severity IOC hits force
exit 1 unconditionally; structural drift exits 1 unless
--allow-drift.
A user without Cursor installed sees no cursor-extensions/
entries — the walker filters to roots that actually exist at
call time. --home <DIR> overrides $HOME for tests / CI.
This is the sideload backstop: even if an attacker bypasses
the marketplace fetch entirely (drag-and-drop .vsix, git clone directly into the extensions dir, vendored install), the
diff catches the new files. Pair with the proxy's
extensionquery rewriter for the fetch path and you cover both
the marketplace-bound and out-of-band install routes.
Static analysis for .github/workflows/*.yml. Walks every uses:
in the workflow and flags any reference that isn't pinned to a
40-char commit SHA — the supply-chain analogue of an unpinned
dependency. Offline by default; opt into the GitHub API with
--resolve when you want the suggested replacement SHA inline.
sakimori actions audit .github/workflows/*.yml
# Machine-readable.
sakimori actions audit --format json .github/workflows/ci.yml
# Treat first-party (actions/*, github/*) mutable refs as blocking
# too — useful once you've already pinned all your third-party deps.
sakimori actions audit --strict .github/workflows/*.yml
# Look up the current SHA each mutable @<ref> resolves to via the
# GitHub REST API. Reads $GITHUB_TOKEN from the env to lift the
# rate limit from 60/hour to 5000/hour. The output gets a
# `→ resolved: <sha>` line per finding (text) or a `resolved_sha`
# field (JSON) so you can copy-paste the right pinned form.
sakimori actions audit --resolve .github/workflows/*.ymlSeverity:
| when | |
|---|---|
| error | third-party action with mutable tag/branch (foo/bar@v1, foo/bar@main) |
| warn | first-party (actions/*, github/*) mutable tag, or docker image without @sha256: digest |
| ok | 40-char SHA pin, local action (./...), docker image with digest |
Exit code: 1 when at least one error is present (or any warn,
under --strict); 0 otherwise. Composite-action action.yml
files are ignored — only workflow files (those with a top-level
jobs: block) are walked. Resolution failures (rate-limit, removed
action) appear as → resolve failed: … per finding without
aborting the audit.
Workflow-level lint (in addition to per-uses: SHA pinning):
the auditor also flags the pull_request_target + writable Actions
cache pattern — the TanStack 2025 cache-poisoning vector. If a
workflow runs on pull_request_target (or workflow_run) and
any job step writes to the GitHub Actions cache, that's an Error.
sakimori actions audit .github/workflows/bundle-size.yml
# .github/workflows/bundle-size.yml (1 ok, 0 warn, 0 error)
# ERROR [pull_request_target_with_cache_write] workflow runs on
# `pull_request_target` and writes to the Actions cache —
# an untrusted fork PR can poison the cache that a later
# trusted workflow restores (TanStack-style npm supply-chain
# compromise). …
# · size (actions/cache@v4): actions/cache writes via post-step on cache missDetected cache writers: actions/cache@*, actions/cache/save@*,
actions/setup-{node,python,java,dotnet,ruby} with with.cache:,
actions/setup-go (caches by default), Swatinem/rust-cache,
mozilla-actions/sccache-action, astral-sh/setup-uv with
enable-cache: true. Cache writes use a runner-internal token, not
the workflow GITHUB_TOKEN, so permissions: contents: read does
not block them. Split cache-writing steps into a separate
workflow that doesn't run on fork PRs, or gate the offending job
behind if: github.event.pull_request.head.repo.full_name == github.repository. JSON output puts these under a top-level
workflow_findings array alongside the per-uses: findings.
Wraps a command under eBPF (Linux) / ETW (Windows) supervision and observes — optionally denies — its syscalls:
connect(2)on IPv4 / IPv6openat(2)execve(2)
sakimori run \
--policy .github/sakimori.yml \
--mode audit \
--log sakimori.log.json \
--html sakimori-report.html \
-- cargo testFlags:
| flag | env | default | description |
|---|---|---|---|
--policy / -p |
SAKIMORI_POLICY |
— | policy file (YAML or JSON) |
--mode |
— | from policy | audit or block — overrides the policy's mode: |
--log |
— | - (stdout) |
JSON audit log destination |
--summary |
GITHUB_STEP_SUMMARY |
— | markdown summary |
--html |
— | — | self-contained HTML report (dark-mode aware, filterable) |
--snapshot-workspace |
— | — | dir to hash before/after the run; drift goes into the JSON log + step summary, and (in block mode) makes the run fail |
--snapshot-skip |
— | — | extra dir basenames to skip during the snapshot (repeatable) |
--snapshot-extensions |
— | — | snapshot every editor-extension dir under $HOME before + after the run; drift + pre-existing IOC + drift-time IOC sections land in the JSON log under extension_drift / extension_iocs / extension_iocs_baseline. High-severity IOC fails the run unconditionally; structural drift fails the run only in block mode |
Exit code: child's exit code, unless mode=block and either:
- at least one event was denied, or
- a
--snapshot-workspacebaseline was taken and the post-run diff is non-empty
→ exits 1 either way.
Policy format:
# .github/sakimori.yml
mode: block # audit | block
network:
# default is `deny`, so only listed destinations can be reached.
allow:
- target: api.github.com # A+AAAA resolved at startup
ports: [443]
- target: 140.82.112.0/20 # CIDR expanded (up to /16 for v4)
ports: [22, 443]
- target: 2606:4700::/48 # IPv6 CIDRs work too
ports: [443]
file:
default: allow # most builds open hundreds of files
deny:
- /etc/shadow
- /root/.ssh
process:
deny_exec:
- /usr/bin/nc
env:
# Scrub the env block before the child execs. Real prevention,
# not a tripwire — `Command::env_clear()` happens before
# `execve`, so the child (and its postinstall grandchildren)
# literally cannot read what's been stripped.
default: pass # `pass` keeps everything not on `deny`;
# `clear` flips it to allowlist mode
allow: [PATH, HOME, "GITHUB_*"]
deny: ["AWS_*", "*_TOKEN", "*_SECRET", NPM_TOKEN]First-time setup pattern — run in mode: audit once, then let
policy suggest turn the log into a starter policy, prune by hand,
and flip to mode: block:
coronarium run --mode audit --log audit.json -- cargo test
coronarium policy suggest audit.json -o .github/coronarium.yml
$EDITOR .github/coronarium.yml # remove anything you don't want allowed
coronarium run -p .github/coronarium.yml --mode block -- cargo testsuggest populates network.allow (one entry per host:port observed,
hostnames preferred over raw IPs) and file.allow (one entry per
parent directory observed). Exec targets are surfaced as a
commented # observed_exec: block — process.deny_exec is
deliberately left empty because the suggester can't know which of
the binaries the build actually wanted.
Curated rule packs (policy preset): ready-to-merge YAML blocks
for known supply-chain attack patterns. Currently shipped:
sakimori policy preset persistence—file.denytripwire for OS-level persistence writes (launchd / systemd / cron / shell rc /~/.ssh). Per-user paths expand from$HOME(override with--home /path); system paths always included.sakimori policy preset cloud-secret-egress—network.denytripwire for AWS / GCP / Azure IMDS and STS-style secret endpoints. Pairs withsakimori proxy start --network-allow ...for SNI-level enforcement.
Both presets print to stdout (or -o policy.yml) with explanatory
comment headers so the operator can pick the entries that fit their
threat model and merge into an existing policy. The persistence
preset ships in mode: audit because its full list exceeds the
Linux 8-entry kernel cap on file.deny under mode: block; to
enforce, prune to your 8 most critical paths and flip the mode:
field to block. The cloud-secret-egress preset ships in
mode: block (no cap on network.deny).
Known-IOC workspace scan (workspace scan-iocs): walk a
workspace and flag files whose path / basename / content matches
a known supply-chain compromise fingerprint (e.g. .claude/ setup.mjs dropped by the Shai-Hulud npm worm; basename .npmrc
for token-exfil; content needles for webhook.site,
discord.com/api/webhooks/, requestbin.com). Distinct from
workspace diff — diff catches "something changed during the
build," scan-iocs catches "this file exists at all, which it
shouldn't." The catalog is bundled in the binary (versioned;
CATALOG_VERSION in sakimori-core::iocs). Exits non-zero on
any High-severity hit; --strict escalates Medium-severity hits
to exit 1 too. Same skip list as workspace snapshot (.git,
node_modules, target, …); extend with --skip <NAME>.
sakimori workspace scan-iocs $GITHUB_WORKSPACE
sakimori workspace scan-iocs . --format json
sakimori workspace scan-iocs . --strict --skip my-build-artefactscan-iocs is also wired into workspace diff, sakimori run --snapshot-workspace, and sakimori daemon start --workspace-baseline … automatically — every added / modified
path in the drift report is scanned against the same catalog and
the findings land in the JSON log under workspace_iocs. A
High-severity hit forces exit 1 in any mode (Audit too); --allow- drift does not suppress it.
The bundled catalog is the only source today. A signed-YAML
refresh path (sakimori iocs update) is a roadmap item — see
CLAUDE.md Roadmap #18 for the planned surface.
The HTML report includes:
- verdict (ALLOW / DENY), kind, pid, comm
- host column (PTR-resolved reverse DNS for connect events)
- detail (IP:port / filename / exec argv)
- filter box matching across all fields
- dark-mode aware, self-contained (no external CSS/JS)
Per-event source attribution (Linux): the supervisor walks
/proc/<pid>/{status,cmdline} PPid chains at event time and tags
each event with the originating package manager (npm, pnpm, yarn,
cargo, pip, uv, poetry, dotnet, go, maven, gradle, bundler,
composer). That shows up as a source: { package_manager, root_argv, chain } field on every JSON-log event and as a "Sources" top-N
table in the step summary, so a connect to evil.example reads as
"came from npm install foo@1.2.3" rather than just "from pid
12345 (sh)". Best-effort — pids that have already exited by the
time the userspace drain reads the ringbuf get source: null and
fall into the (unattributed) row. Windows ETW supervisor doesn't
attach attribution yet.
Works on Linux, macOS, and Windows GitHub-hosted runners (Windows
requires sakimori v0.34.3 or newer — earlier Windows release tarballs
ship only sakimori-win.exe, the ETW supervisor, which has no proxy
subcommand). The proxy starts in the background as the action's main
step, exports HTTPS_PROXY + the CA bundle for every common HTTPS
client via $GITHUB_ENV, and survives across run: step boundaries
until the post-step kills it at end-of-job.
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
# Spawns `sakimori proxy start` detached and appends
# HTTPS_PROXY / CARGO_HTTP_CAINFO / NODE_EXTRA_CA_CERTS /
# PIP_CERT / REQUESTS_CA_BUNDLE / SSL_CERT_FILE to $GITHUB_ENV
# for every step after this one.
- uses: bokuweb/sakimori/proxy@v0
with:
min-age: 7d
- run: npm ci # routed through the proxy
- run: cargo test # routed through the proxy
- run: pip install -r requirements.txt # routed through the proxyInputs:
| input | default | description |
|---|---|---|
min-age |
7d |
Minimum package age. Same grammar as --min-age. |
listen |
127.0.0.1:8910 |
Proxy listen address. |
fail-on-missing |
false |
Treat unknown publish dates as deny. |
version |
v0 |
sakimori release tag to download. |
token |
${{ github.token }} |
Used by gh release download. |
Outputs:
| output | description |
|---|---|
ca-cert |
Absolute path to the proxy's root CA PEM. Also exported via $GITHUB_ENV as CARGO_HTTP_CAINFO, PIP_CERT, NODE_EXTRA_CA_CERTS, REQUESTS_CA_BUNDLE, and SSL_CERT_FILE. |
Cheaper (no proxy), but fails loudly on any too-young dep instead of silently falling back.
- uses: bokuweb/sakimori@v0
- run: $SAKIMORI_BIN deps check --min-age 7d Cargo.lock package-lock.json
- run: cargo test # only reached if the check passedThe proxy filters at fetch time — it can't see bytes restored
from actions/cache. If your workflow uses actions/cache (or
actions/setup-node with cache:, Swatinem/rust-cache, etc.) a
poisoned restore happens between cache-restore and install, behind
the proxy's back.
Drop this step in right after install to re-hash every blob in
the local store against the lockfile's integrity: fields:
- uses: bokuweb/sakimori/proxy@v0
with: { min-age: 7d }
- uses: actions/cache@v4
with: { path: ~/.local/share/pnpm/store, key: ... }
- run: pnpm install # populates / hits the cache
# ↓ catches TanStack-style cache poisoning: cache restored a
# tarball whose bytes don't match what the lockfile pinned.
- uses: bokuweb/sakimori/verify-cache@v0
with:
lockfile: pnpm-lock.yamlSupports package-lock.json, pnpm-lock.yaml, and Cargo.lock;
auto-picks the cache root for the runner OS. Inputs:
| input | default | description |
|---|---|---|
lockfile |
(required) | Path to package-lock.json, pnpm-lock.yaml, or Cargo.lock. |
cache |
(auto) | Override the store root. Auto-detected from the runner OS — ~/.npm/_cacache (Linux/macOS) or %LOCALAPPDATA%\npm-cache\_cacache (Windows) for npm; ~/.local/share/pnpm/store/v3 / ~/Library/pnpm/store/v3 / %LOCALAPPDATA%\pnpm\store\v3 for pnpm; $CARGO_HOME (default ~/.cargo or %USERPROFILE%\.cargo) for cargo. |
format |
text |
text or json. |
version |
v0 |
sakimori release tag. |
token |
${{ github.token }} |
Used by gh release download. |
Exit codes match the CLI: 0 clean, 1 on any mismatch / missing
entry. pnpm v11+ SQLite stores are not yet supported — the
action exits with a clear Unsupported error rather than passing
silently. (v10 still uses the JSON layout and works fine.)
Use bokuweb/sakimori/job@v0 when you want a single audit log covering
every step in the job instead of just one wrapped command. The
action's pre-hook spawns a background eBPF supervisor attached to the
runner-worker's cgroup; cgroup v2 inheritance means every step the
runner forks afterwards (actions/checkout, your run: blocks,
actions/upload-artifact, ...) is observed by the same supervisor.
The post-hook flushes the JSON log / step summary / HTML report and
fails the job if mode: block denied anything.
runs-on: ubuntu-latest
steps:
- uses: bokuweb/sakimori/job@v0 # MUST come before checkout so the
with: # supervisor is up first
policy: .github/sakimori.yml
mode: block
html: sakimori-report.html
- uses: actions/checkout@v4
- run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm test
# post-hook of bokuweb/sakimori/job runs here automaticallyLimitations: Linux runners only (Windows needs a different kernel
hook), and container jobs (jobs.<id>.container:) are unsupported
because the host-side cgroup attach can't reach steps that run inside
the container. Matrix shards and reusable-workflow callers are each
their own job and need their own bokuweb/sakimori/job@v0.
Uploading the audit log from the same job: the daemon writes
its JSON / HTML / step-summary at end-of-job (the post-hook), which
is too late for an actions/upload-artifact step inside the same
job. Drop in bokuweb/sakimori/job/stop@v0 right before the
upload to flush the daemon early:
- uses: bokuweb/sakimori/job@v0
with: { policy: .github/sakimori.yml, mode: block }
- uses: actions/checkout@v4
- run: pnpm test
- uses: bokuweb/sakimori/job/stop@v0 # flush + stop
- uses: actions/upload-artifact@v4
with:
name: sakimori-report
path: |
sakimori.log.json
sakimori-report.htmlIt's idempotent — the daemon's own post-hook turns into a no-op on the missing pid-file. On non-Linux matrix entries the sub-action no-ops silently, so it's safe to drop into a cross-OS workflow.
Tamper detection: pass snapshot-workspace: <DIR> to also catch
on-disk tampering. The daemon can't take the baseline itself (it
starts before checkout), so add a tiny step right after checkout that
records the baseline — the action exports the paths for you:
- uses: bokuweb/sakimori/job@v0
with:
policy: .github/sakimori.yml
mode: block
snapshot-workspace: .
- uses: actions/checkout@v4
- run: sudo -E "$SAKIMORI_BIN" workspace snapshot
"$SAKIMORI_WORKSPACE_DIR" -o "$SAKIMORI_BASELINE_PATH"
- run: pnpm install --frozen-lockfile
- run: pnpm buildThe daemon re-snapshots $SAKIMORI_WORKSPACE_DIR at post-time, diffs
against the baseline, and surfaces drift in the JSON log + step
summary. Forgetting the snapshot step is non-fatal (the daemon logs a
warning and the drift section is omitted).
The simplest form: pass the command you want supervised via the
run: input. The action installs sakimori AND wraps the command
with sakimori run for you — no separate sudo -E env "PATH=$PATH" "$SAKIMORI_BIN" run … step required.
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: bokuweb/sakimori@v0
with:
policy: .github/sakimori.yml
mode: audit
html: sakimori-report.html
run: |
corepack enable
cargo test
pnpm install --frozen-lockfile
pnpm testOn Linux the script runs under
sudo -E env "PATH=$PATH" "$SAKIMORI_BIN" run … -- bash -euxo pipefail -c '<run>';
on Windows under & $env:SAKIMORI_BIN … -- pwsh -NoProfile -Command "<run>".
--summary defaults to $GITHUB_STEP_SUMMARY and --log defaults
to the log: input (sakimori.log.json). Add `snapshot-workspace:
If you need more control over the wrapper invocation, omit run:
and write the sakimori run step yourself. The action exports
$SAKIMORI_BIN, $SAKIMORI_POLICY, $SAKIMORI_MODE, and
$SAKIMORI_LOG for you.
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: bokuweb/sakimori@v0
with:
policy: .github/sakimori.yml
mode: audit
- if: runner.os == 'Linux'
run: |
# `sudo -E` preserves env *except* PATH (sudo always replaces
# it with secure_path). `env "PATH=$PATH"` re-injects the
# runner user's PATH so the supervised child can find tools
# installed outside /usr/bin (pnpm, cargo, rustup toolchains).
sudo -E env "PATH=$PATH" "$SAKIMORI_BIN" run \
--policy "$SAKIMORI_POLICY" \
--mode "$SAKIMORI_MODE" \
--log "$SAKIMORI_LOG" \
--html sakimori-report.html \
--summary "$GITHUB_STEP_SUMMARY" \
-- cargo test
- if: runner.os == 'Windows'
shell: pwsh
run: |
& $env:SAKIMORI_BIN `
--policy $env:SAKIMORI_POLICY `
--log sakimori.log.json `
--html sakimori-report.html `
-- cargo test
- uses: actions/upload-artifact@v4
if: always()
with:
name: sakimori-report-${{ runner.os }}
path: |
sakimori-report.html
sakimori.log.jsonbokuweb/sakimori/comment@v0 reads the JSON log and upserts a
single PR comment (keyed by an HTML marker, re-runs edit in place).
Embeds a gh run download one-liner to view the full HTML on your
machine.
- uses: bokuweb/sakimori/comment@v0
if: github.event_name == 'pull_request'
with:
log: sakimori.log.json
artifact-name: sakimori-report
html-filename: sakimori-report.html
# fail-on-denied: "true" # optional| runner | proxy | supervised run | notes |
|---|---|---|---|
ubuntu-latest, ubuntu-22.04, ubuntu-24.04 |
✅ | ✅ | canonical Linux target, eBPF + tracepoints |
ubuntu-24.04-arm |
✅ | ✅ | aarch64 binary ships in each release |
windows-latest |
✅ | ✅ | ETW public providers; elevated by default |
windows-2022, windows-2019 |
✅ | probably works but not smoke-tested | |
macos-latest |
✅ | ❌ | supervised mode is Linux/Windows only |
container jobs (container: on Linux) |
✅ | needs --privileged + host cgroup mount |
|
| self-hosted Linux | ✅ | needs passwordless sudo, kernel ≥ 5.13 | |
| self-hosted Windows | ✅ | needs Administrator for ETW |
Prebuilt multi-arch image on GHCR:
docker pull ghcr.io/bokuweb/sakimori-proxy:v0Tags: v0 (floating), v0.N, v0.N.M, latest. Available archs:
linux/amd64, linux/arm64.
Run with a named volume so the CA persists across restarts:
docker run --rm -p 8910:8910 \
-v sakimori-conf:/etc/sakimori-xdg \
ghcr.io/bokuweb/sakimori-proxy:v0 \
--listen 0.0.0.0:8910 --min-age 7d
# One-shot: grab the generated CA so hosts can trust it.
docker run --rm -v sakimori-conf:/etc/sakimori-xdg \
--entrypoint cat ghcr.io/bokuweb/sakimori-proxy:v0 \
/etc/sakimori-xdg/sakimori/ca.pem > /tmp/sakimori-ca.pemThen on each host:
export HTTPS_PROXY=http://<container-host>:8910
export CARGO_HTTP_CAINFO=/tmp/sakimori-ca.pem
# (or install-ca into your OS trust store with the CA you just copied)Integer + unit. Bare numbers default to days.
| suffix | unit |
|---|---|
d |
days |
h |
hours |
m |
minutes |
s |
seconds |
Examples: 7d, 72h, 30m, 3600s, 7 (= 7 days).
| OS | CA + key | Cache | Daemon unit |
|---|---|---|---|
| macOS | ~/.config/sakimori/ca.{pem,key} (or $XDG_CONFIG_HOME) |
~/Library/Caches/sakimori/deps-cache.json |
~/Library/LaunchAgents/com.sakimori.proxy.plist |
| Linux | $XDG_CONFIG_HOME/sakimori/ca.{pem,key} |
$XDG_CACHE_HOME/sakimori/deps-cache.json |
~/.config/systemd/user/sakimori-proxy.service |
| Windows | %LOCALAPPDATA%\sakimori\ca.{pem,key} |
%LOCALAPPDATA%\sakimori\deps-cache.json |
%LOCALAPPDATA%\sakimori\sakimori-proxy.task.xml |
| var | purpose |
|---|---|
SAKIMORI_POLICY |
Default policy file for run / check-policy |
SAKIMORI_MODE |
Override policy mode in run |
SAKIMORI_LOG |
Default log destination in run |
SAKIMORI_BIN |
Set by the GH Action install step |
SAKIMORI_BPF_OBJ |
Path to sakimori.bpf.o (Linux only) |
GITHUB_STEP_SUMMARY |
Default --summary target |
XDG_CONFIG_HOME / XDG_CACHE_HOME |
Override default config/cache dir |
- Check it's actually running:
pgrep -f 'sakimori proxy' - On macOS:
launchctl list | grep sakimori - On Linux:
systemctl --user status sakimori-proxy - On Windows:
schtasks /Query /TN sakimori-proxy - Try
sakimori proxy startin the foreground — see the log.
Cargo on Linux uses libcurl which doesn't read the system trust
store — CARGO_HTTP_CAINFO must point at the sakimori CA.
Likewise PIP_CERT for pip and NODE_EXTRA_CA_CERTS for npm.
install-gate install sets all of these. If you skipped that,
either install-gate now or set them manually.
macOS keychain writes need sudo. Re-run with sudo, or copy the
printed security add-trusted-cert … line and run it yourself.
- Is the proxy running?
sakimori doctor - Is
HTTPS_PROXYset in this shell? (install-gate only applies to new shells.)echo $HTTPS_PROXY - Is the package being downloaded from a host sakimori
intercepts? By default only the canonical public hosts
(
registry.npmjs.org,pypi.org,files.pythonhosted.org,crates.io,index.crates.io,api.nuget.org) are watched. Internal mirrors / replacement registries need to be added — see Custom / internal registries for the--npm-registry/--registries-configflags.
Run the proxy on a separate host and point client env at it:
export HTTPS_PROXY=http://proxy.corp.internal:8910
export CARGO_HTTP_CAINFO=/etc/sakimori/ca.pem # copy from the proxy containerHonest assessment. Full details in CLAUDE.md.
- Sigstore bundle verification (not just claim presence) is a
roadmap item.
--require-provenancecurrently checks that thedist.attestations.provenance.predicateTypefield is non-empty, which is already meaningful (npm refuses to attach it unless the publish came from OIDC-authenticated CI) but the bundle itself isn't cryptographically verified. - CDN IP rotation across long runs: handled by
sakimori run --dns-refresh-interval <secs>, which re-resolvesnetwork.allow/network.denyhostnames every N seconds and additively inserts new IPs into the eBPF maps. Default15(seconds); set0to disable, raise to 60–300 for very long CI jobs behind round-robin DNS. Entries are never removed once written, so increasing the rate is safe and won't kill active connections.
- Network block works at the kernel (EPERM from cgroup/connect4|6).
- File block is "tripwire" —
bpf_send_signal(SIGKILL)on a matchingopenat. The fd may briefly exist; the process dies before consuming it. For a truly pre-open block we'd needbpf_override_return, which is CONFIG_BPF_KPROBE_OVERRIDE dependent (roadmap). - Exec deny is audit-only — events get
denied: truein the log and block-mode exits non-zero, but the exec itself happens. Same roadmap item as file block. - deps watch is detection, not prevention — FS events fire
after the package manager has already run
postinstall.
network.default: denyis audit-only — Windows Defender Firewall evaluates block over allow, so an allowlist pattern would require flipping the system-wide default-outbound to Block, which we won't do silently.network.deny: […]is kernel-enforced.
- No supervised run mode.
runis Linux/Windows only — on macOS sakimori is a desktop-level tool (proxy + deps + watch).
# Full test suite (core + proxy + install-gate + daemon + doctor)
cargo test --workspace
# Lint
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all -- --check
# Build the eBPF object (Linux only, requires nightly + bpf-linker)
rustup toolchain install nightly --component rust-src
cargo install bpf-linker
cd crates/sakimori-ebpf
RUSTUP_TOOLCHAIN=nightly cargo build --release \
--target bpfel-unknown-none -Z build-std=coreCrates:
sakimori-common—no_stdPOD types shared with eBPF (ring buffer records, map keys)sakimori-core— platform-neutral: events, policy, matcher, stats, HTML report,deps::*, watchsakimori-ebpf— Linux kernel programs (cgroup/connect tracepoints). Excluded from the main workspace.sakimori-proxy— HTTPS MITM proxy (hudsucker + rustls), registry parsers, rewriters (crates/npm/pypi/nuget), CA management, daemon unit generatorssakimori— userspace CLI and Linux supervisorsakimori-win— Windows ETW supervisor + Defender Firewall integration (separate workspace for dep isolation)
Architecture notes live in CLAUDE.md.
sakimori is free to use under MIT/Apache-2.0. If your team needs any of the following, the maintainer offers paid engagements:
- Onboarding help (writing/auditing your
policy.yml, integrating with your CI, tuning per-runner thresholds). - Priority bug fixes and feature requests.
- Private Slack/Discord channel for questions.
- Custom ecosystem support or proprietary registry adapters.
Contact: bokuweb12@gmail.com
For non-commercial appreciation, GitHub Sponsors is also welcome.
See CONTRIBUTING.md. All commits must be signed off
(DCO) — git commit -s.
MIT. See LICENSE.