{"id":51193864,"url":"https://github.com/bokuweb/sakimori","last_synced_at":"2026-06-27T18:03:11.162Z","repository":{"id":352222280,"uuid":"1214344760","full_name":"bokuweb/sakimori","owner":"bokuweb","description":"Cross-platform supply-chain guard for CI: supervised-run audit/block (eBPF/ETW) + minimum-release-age proxy \u0026 lockfile check for npm, cargo, PyPI, NuGet.","archived":false,"fork":false,"pushed_at":"2026-06-21T17:05:05.000Z","size":6519,"stargazers_count":74,"open_issues_count":5,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-21T17:21:58.684Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bokuweb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null},"funding":{"github":["bokuweb"]}},"created_at":"2026-04-18T12:54:39.000Z","updated_at":"2026-06-04T10:32:39.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bokuweb/sakimori","commit_stats":null,"previous_names":["bokuweb/coronarium","bokuweb/sakimori"],"tags_count":43,"template":false,"template_full_name":null,"purl":"pkg:github/bokuweb/sakimori","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bokuweb%2Fsakimori","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bokuweb%2Fsakimori/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bokuweb%2Fsakimori/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bokuweb%2Fsakimori/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bokuweb","download_url":"https://codeload.github.com/bokuweb/sakimori/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bokuweb%2Fsakimori/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34862630,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-27T02:00:06.362Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-27T18:03:10.385Z","updated_at":"2026-06-27T18:03:11.154Z","avatar_url":"https://github.com/bokuweb.png","language":"Rust","funding_links":["https://github.com/sponsors/bokuweb"],"categories":[],"sub_categories":[],"readme":"# sakimori\n\n[![CI](https://github.com/bokuweb/sakimori/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/bokuweb/sakimori/actions/workflows/ci.yml)\n[![release](https://img.shields.io/github/v/release/bokuweb/sakimori?sort=semver)](https://github.com/bokuweb/sakimori/releases/latest)\n[![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n\n🚧Work In Progress🚧\n\n**Cross-platform supply-chain guard for every package manager on your\nmachine.** Silently blocks too-young versions, known-malicious packages\nand unsigned publishes — across **npm, cargo, pypi, nuget** — without\ntouching your build tools.\n\n```bash\n# Three commands, once.\n$ sakimori proxy install-ca       # trust the proxy's root CA\n$ sakimori proxy install-daemon   # auto-run in the background\n$ sakimori install-gate install   # route your shell through it\n\n# Business as usual, permanently safer.\n$ npm install react\n# → proxy silently drops versions \u003c 7d old\n# → npm picks the newest older version\n# → no error, no broken build, just a measurably safer dependency\n```\n\nWorks on **macOS, Linux, and Windows**. Also ships a CI mode\n(`deps check` + eBPF/ETW supervisor) for pipelines.\n\n- [Why this exists](#why-this-exists)\n- [How it works](#how-it-works) — proxy architecture, 4 ecosystems\n- [Install](#install)\n- [Desktop quick start](#desktop-quick-start)\n- [Feature reference](#feature-reference) — every subcommand with examples\n- [CI usage (GitHub Actions)](#ci-usage-github-actions)\n- [Docker image](#docker-image)\n- [Configuration reference](#configuration-reference)\n- [Troubleshooting](#troubleshooting)\n- [Known limitations](#known-limitations) — what this honestly can't do\n- [Development](#development)\n\n---\n\n## Why this exists\n\nSupply-chain attacks follow a predictable timeline:\n\n1. Attacker publishes a malicious version at `T+0`\n2. Community notices, yanks it at `T+12–72h`\n\nMost victims install between hours 0–12. **pnpm 10.x** introduced\n[`minimumReleaseAge`](https://pnpm.io/next/settings#minimumreleaseage)\nto solve this for npm only — versions younger than the threshold\nbecome invisible to the resolver, which silently falls back to the\nnewest older one.\n\n**sakimori brings the same behaviour to all four major ecosystems**\n(crates.io, npm, pypi, nuget) and any package manager that talks to\nthem, by sitting as an HTTPS proxy and rewriting the registry's\nmetadata responses in-flight. No resolver integration. No config in\nyour package manifests.\n\n## How it works\n\n```\n            ┌───────────────────┐       ┌────────────────────┐\n            │  npm / cargo /    │       │                    │\n  user ───► │  pip / uv /       │ ─────►│  sakimori proxy  │ ──► real registry\n            │  dotnet / poetry  │  HTTPS│  (localhost:8910)  │     (metadata + tarball)\n            └───────────────────┘       └─────────┬──────────┘\n                                                  │\n                                                  ▼\n                                         rewrites metadata:\n                                         - drop versions \u003c --min-age\n                                         - drop unsigned versions (--require-provenance)\n                                         - retarget npm dist-tags.latest\n                                         - returns 403 for pinned tarball fetches\n                                           to too-young versions\n```\n\nThe proxy's root CA is installed into the system trust store once;\nfrom then on, every HTTPS request your package managers make through\n`HTTPS_PROXY=http://127.0.0.1:8910` gets transparently filtered.\n\n### Ecosystem coverage\n\n| ecosystem | silent auto-fallback | hard deny on pinned fetch |\n|---|---|---|\n| **crates.io** | ✅ sparse-index JSONL rewrite (drops too-young lines from `/\u003cprefix\u003e/\u003cname\u003e`) | ✅ `403` on `.crate` download to a denied version |\n| **npm** | ✅ packument rewrite (drops versions + retargets `dist-tags.latest`) | ✅ `403` on `.tgz` download |\n| **pypi** | ✅ Warehouse JSON API (`/pypi/\u003cpkg\u003e/json`) + PEP 691 Simple JSON + PEP 503 Simple HTML via JSON-API lookup | ✅ `403` on `files.pythonhosted.org` tarball download |\n| **nuget** | ✅ registration-page rewrite (`/v3/registration*/...`) + flat-container index via registration lookup | ✅ `403` on `.nupkg` download |\n| **vscode-marketplace** | ✅ `extensionquery` JSON rewrite (drops `versions[].lastUpdated` younger than `--min-age`) on `marketplace.visualstudio.com` + `open-vsx.org` | ✅ `.vsix` lifecycle gate: `403` on startup-autorun (`activationEvents: [\"*\" | \"onStartupFinished\"]`), on any bundled `node_modules/*` package listed by OSV / GHSA as malicious, and on High-severity IOC content hits inside the archive ([roadmap #21 / #25 / #26](CLAUDE.md)) |\n\nAll five ecosystems' metadata paths now rewrite silently — pnpm-style\n`minimumReleaseAge` across the board, no fail-hard in the common case.\n\n### OS support matrix\n\nsakimori has two layers, and they have **different** OS coverage. Read\nthis carefully before assuming \"macOS isn't supported\" or \"Linux gets\neverything\":\n\n| capability | Linux | macOS | Windows |\n|---|---|---|---|\n| **Fetch-layer** (proxy + install-gate) | | | |\n| ↳ `sakimori proxy start` (MITM + age filter + auto-fallback) | ✅ | ✅ | ✅ |\n| ↳ `sakimori install-gate install` (shell wiring) | ✅ zsh / bash / fish | ✅ zsh / bash / fish | ✅ PowerShell |\n| ↳ `~/.sakimori/installs.jsonl` recording — *who installed what, when* | ✅ | ✅ | ✅ |\n| ↳ `sakimori advisories scan` (OSV JOIN over the install log) | ✅ | ✅ | ✅ |\n| ↳ Lifecycle-script inspection (`--lifecycle-policy audit|block`) | ✅ | ✅ | ✅ |\n| ↳ `sakimori deps check` / `verify-cache` / `watch` | ✅ | ✅ | ✅ |\n| **Supervisor-layer** (`sakimori run` / `daemon`) | | | |\n| ↳ exec / open / connect events | ✅ eBPF | ❌ planned ([roadmap 5b](CLAUDE.md)) | ✅ ETW |\n| ↳ PPid attribution → package-manager origin | ✅ | ❌ | partial |\n| ↳ `--snapshot-workspace` (drift + known-IOC scan at shutdown) | ✅ | ❌ | partial |\n| ↳ Live network block | ✅ eBPF cgroup hooks | ❌ planned (#5) | ✅ Defender Firewall |\n| ↳ Live file/exec block | tripwire (SIGKILL); pre-syscall in progress (#4) | ❌ | audit-only |\n\n**Headline**: *if you only care about \"tell me which packages I\ninstalled and warn me when one of them gets a CVE next week\", macOS is\na first-class platform.* That's the part most users want. The\nsupervisor (live blocking, exec attribution, workspace drift) is where\nthe Mac gap is — tracked as roadmap item 5b in CLAUDE.md (Apple's\nEndpoint Security framework, requires Apple-issued entitlement +\nSystemExtension signing).\n\nCI coverage matches: the [`macos-smoke` workflow](.github/workflows/macos-smoke.yml)\nexercises proxy start → pinned-tarball fetch → `installs.jsonl` →\n`advisories scan` → `install-gate shellenv` end-to-end on every PR\nthat touches the relevant crates, so the cells marked ✅ for macOS\nabove don't silently regress.\n\n### Editor-extension coverage (VSCode / Cursor / Windsurf / OpenVSX)\n\nThe 2026-05 GitHub-internal-repo compromise (poisoned VS Code\nextension on an employee device) made it concrete: editor and\nbrowser extensions are a parallel distribution channel that almost\nnothing on the supply-chain market actually catches end-to-end.\nsakimori covers it across four layers, each addressing a failure\nmode the others can't:\n\n| layer | what catches | where in sakimori |\n|---|---|---|\n| **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 |\n| **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 |\n| **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 |\n| **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 |\n| **Bundled-dep poisoning** | An extension whose top-level manifest is clean but that ships a malicious transitive dep inside its `extension/node_modules/` tree | `.vsix` lifecycle gate walks the bundled tree, emits one `InstallEvent { ecosystem: npm }` per nested `package.json` (so they show up in `installs.jsonl` + OSV scan + OTLP / hub fan-out), and `403`s the install under `--lifecycle-policy block` when any bundled `(name, version)` is in the OSV known-bad set #25 |\n| **In-`.vsix` payload IOC** | Bundled JS / JSON that embeds a known exfil destination (`webhook.site`, `discord.com/api/webhooks/`, …) — including in transitive deps the publisher never audited | `.vsix` lifecycle gate runs the `sakimori-core::iocs` content-needle catalog over every text-shaped archive entry on fetch; High-severity hits `403` the install under `--lifecycle-policy block` and surface in the audit log regardless of mode #26 |\n\n#### Why this is different from \"be a marketplace mirror\"\n\nA few existing tools in this space try to be a **registry mirror**\nfor the VS Code Marketplace — they stand up a server, scrape\nMicrosoft's gallery, and ask users to repoint VS Code at the\nmirror via `product.json` edits or `extensions.gallery.serviceUrl`\noverrides. That approach has real friction:\n\n1. **VS Code has no first-class \"alternate registry\" config.** The\n   marketplace URL is hardcoded in `product.json`; switching it\n   requires modifying Microsoft's binary, which the EULA forbids\n   redistributing. (VSCodium, Cursor, Code-OSS, Windsurf — forks —\n   *do* expose a configurable gallery setting; the EULA issue is\n   specific to upstream VS Code.)\n2. **Marketplace ToS treats redistribution carefully.** §3 of the\n   VS Code Marketplace terms allows access to the gallery for\n   downloading extensions; standing up an independent mirror that\n   *re-serves* the gallery to other users sits in genuinely murky\n   territory. Several mirror-style projects have hit takedown notices\n   or quietly become enterprise-only because of this.\n\n**sakimori takes a different shape — an endpoint MITM proxy, not a\nmirror.** That sidesteps both problems:\n\n- **No editor binary modification.** The user sets\n  `HTTPS_PROXY=http://127.0.0.1:8080` (via `sakimori install-gate\n  install`, the same one-liner `npm install` already uses) and\n  trusts sakimori's local CA. The marketplace request the editor\n  makes is unchanged; only the *response* the editor receives is\n  filtered to drop too-young versions. `product.json` stays\n  byte-for-byte identical to whatever Microsoft shipped.\n- **No re-serving.** sakimori never stands up an authoritative\n  gallery. It MITMs the user's *own* request to Microsoft (or\n  Eclipse for OpenVSX), filters the JSON in transit, and hands\n  it back. The bytes leave sakimori the moment the editor reads\n  them; no per-tenant caching or republication. Architecturally\n  this is the same posture Bitdefender, Kaspersky, Cisco\n  Umbrella, and corporate SSL-inspection appliances have run for\n  a decade — well-understood, both legally and operationally.\n- **Same proxy, every editor.** One running instance covers VS\n  Code, Cursor, VSCodium, Code-OSS, code-server, and any other\n  editor that speaks the Marketplace / OpenVSX `extensionquery`\n  API. No per-editor configuration knob.\n\n#### Defence in depth, not just the proxy\n\nThe proxy alone doesn't solve the problem — a determined attacker\nships an extension *with* a deliberate publication delay so it\nclears `--min-age`, or sideloads via `.vsix` to bypass the proxy\nentirely. That's why the four-layer table above matters:\n\n- A sideloaded extension never hits the proxy → `extensions diff`\n  still catches it (new files under `~/.vscode/extensions/`).\n- An aged-into-marketplace extension passes the rewriter →\n  attribution + persistence-write rule pack catches the actual\n  malicious behaviour (writing to `~/.ssh/`, hitting IMDS, etc.)\n  at runtime.\n- A workspace `.vscode/tasks.json` autorun never touches the\n  marketplace at all → the IOC catalog catches the dropper\n  primitive directly.\n\nRegistry-firewall tools (Sonatype Nexus Firewall, JFrog Xray) sit\nat the right layer for #1 but only for the proxy path. EDR /\nSCA tools cover none of the four directly. sakimori is the only\nendpoint agent we're aware of that covers all four with a\nshared attribution + IOC backbone.\n\n#### Roadmap items pending in this area\n\n`.vsix` / `.crx` lifecycle gate (block on `activationEvents:\n[\"*\"]`-style autorun primitives at fetch time), `Ecosystem::\nVscodeExtension` propagation into the install log and OSV-JOIN\nadvisory scan, and Chrome Web Store coverage are all tracked in\n[CLAUDE.md](CLAUDE.md) roadmap entries #20–#24. Pull requests\nwelcome.\n\n---\n\n## Install\n\nPick whichever fits your setup.\n\n### Homebrew (macOS / Linux)\n\n```bash\nbrew install bokuweb/sakimori/sakimori\n# ↑ the repo-is-its-own-tap convention; no separate `brew tap` needed.\n```\n\nAuto-updated on every release via the `homebrew-formula.yml`\nworkflow — the formula lives at\n[`HomebrewFormula/sakimori.rb`](HomebrewFormula/sakimori.rb)\nin this repo.\n\n### Pre-built binary (macOS / Linux / Windows)\n\n```bash\n# macOS (Apple Silicon)\ncurl -fsSL https://github.com/bokuweb/sakimori/releases/latest/download/sakimori-aarch64-apple-darwin.tar.gz \\\n  | sudo tar -xz -C /usr/local/bin\n\n# macOS (Intel)\ncurl -fsSL https://github.com/bokuweb/sakimori/releases/latest/download/sakimori-x86_64-apple-darwin.tar.gz \\\n  | sudo tar -xz -C /usr/local/bin\n\n# Linux (x86_64 musl static)\ncurl -fsSL https://github.com/bokuweb/sakimori/releases/latest/download/sakimori-x86_64-unknown-linux-musl.tar.gz \\\n  | sudo tar -xz -C /usr/local/bin\n\n# Windows (PowerShell)\nInvoke-WebRequest -Uri https://github.com/bokuweb/sakimori/releases/latest/download/sakimori-x86_64-pc-windows-msvc.tar.gz -OutFile c.tgz\ntar -xzf c.tgz -C \"$env:USERPROFILE\\.local\\bin\"\n```\n\nEvery release also ships a `.sha256` sidecar. The archive contains\nthe `sakimori` binary (Linux also ships `sakimori.bpf.o` for the\nsupervised-run mode).\n\n### Docker / OCI\n\n```bash\ndocker run --rm -p 8910:8910 \\\n    -v sakimori-conf:/etc/sakimori-xdg \\\n    ghcr.io/bokuweb/sakimori-proxy:v0 \\\n    --listen 0.0.0.0:8910 --min-age 7d\n```\n\nMount `/etc/sakimori-xdg` as a volume to persist the generated\nroot CA across container restarts. See [Docker image](#docker-image).\n\n### From source\n\n```bash\ncargo install --git https://github.com/bokuweb/sakimori sakimori\n```\n\nThe Linux eBPF supervised-run mode additionally needs\n`rustup toolchain install nightly --component rust-src` +\n`cargo install bpf-linker`. Not required for proxy / deps / install-gate.\n\n---\n\n## Desktop quick start\n\nThree commands, once per machine. Each is idempotent.\n\n```bash\n# 1. Generate the proxy's root CA and install it into the system\n#    trust store. macOS uses `security`, Linux uses\n#    `update-ca-certificates`, Windows uses elevated\n#    `Import-Certificate` (triggers one UAC prompt).\nsakimori proxy install-ca\n\n# 2. Register the proxy as a background service so it's always up.\n#    macOS: ~/Library/LaunchAgents/com.sakimori.proxy.plist\n#    Linux: ~/.config/systemd/user/sakimori-proxy.service\n#    Windows: Task Scheduler /sakimori-proxy\nsakimori proxy install-daemon\n# Follow the printed `launchctl bootstrap …` / `systemctl --user enable --now`\n# / `schtasks.exe /Create …` line.\n\n# 3. Append HTTPS_PROXY + CA bundle env vars to your shell rc.\n#    Detects zsh / bash / fish / PowerShell from $SHELL (or your OS).\nsakimori install-gate install\n```\n\nOpen a new shell — everything's wired:\n\n```\n$ env | grep -E 'HTTPS_PROXY|CARGO_HTTP_CAINFO'\nHTTPS_PROXY=http://127.0.0.1:8910\nCARGO_HTTP_CAINFO=/Users/you/.config/sakimori/ca.pem\n\n$ sakimori doctor\nsakimori doctor\n────────────────────────────────────────────────────────────\n✓ CA certificate               /Users/you/.config/sakimori/ca.pem (644 bytes)\n✓ CA private key               /Users/you/.config/sakimori/ca.key\n✓ Proxy reachable              accepted TCP on 127.0.0.1:8910\n✓ $HTTPS_PROXY                 http://127.0.0.1:8910\n✓ install-gate rc              /Users/you/.zshrc\n✓ Daemon unit                  /Users/you/Library/LaunchAgents/com.sakimori.proxy.plist\n────────────────────────────────────────────────────────────\n6 check(s): 0 fail, 0 warn\n```\n\nFrom here, `npm install` / `pnpm add` / `yarn add` / `cargo add` /\n`cargo build` / `pip install` / `uv add` / `poetry add` /\n`dotnet add package` / `dotnet restore` all flow through the proxy.\n\n### Observable proof that it works\n\n```bash\n$ curl -s https://index.crates.io/se/rd/serde | wc -l           # direct\n315\n\n$ curl -sx http://127.0.0.1:8910 https://index.crates.io/se/rd/serde | wc -l\n306     # the 9 most recent versions are invisible to cargo's resolver\n```\n\ncargo picks the newest remaining in-range version — no error, just\nsafer. Same shape on the other three ecosystems.\n\n### Uninstall\n\nReverse each step (same flags):\n\n```bash\nsakimori install-gate uninstall    # strip block from shell rc\nsakimori proxy uninstall-daemon    # remove launchd / systemd / Task Scheduler unit\nsakimori proxy uninstall-ca        # remove CA from system trust store\nrm -rf ~/.config/sakimori          # delete CA + key (optional)\n```\n\n---\n\n## Feature reference\n\n### `proxy start`\n\nStart the MITM HTTPS proxy in the foreground. `install-daemon`\nwraps this for background use; run it directly when you want logs\non stdout or you're running the proxy yourself in Docker.\n\nRun `sakimori proxy start --help` for the canonical, always-up-to-date\nflag list. The grouping below summarises the surface so you know\nwhat knobs exist; defaults are tuned for \"drop into `~/.zshrc` and\nforget\" desktop use — CI workflows usually want to layer on the\nlifecycle gate and provenance check.\n\n```\nsakimori proxy start [OPTIONS]\n```\n\n| group | flags | what it does |\n|---|---|---|\n| **Networking** | `--listen \u003cADDR\u003e` (default `127.0.0.1:8910`), `--config-dir \u003cPATH\u003e` | Where the proxy listens; where its CA / config files live. |\n| **Release-age gate** | `--min-age \u003cDURATION\u003e` (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). |\n| **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. |\n| **Known-malicious gate** | `--osv`, `--osv-mirror`, `--osv-mirror-url \u003cURL\u003e` | Consult OSV.dev (live) and/or the sakimori-hosted pre-filtered mirror; hard-deny versions tagged MAL-* / known-malicious regardless of `--min-age`. |\n| **Typosquat detection** | `--typosquat {warn,block}`, `--typosquat-mirror`, `--typosquat-mirror-url \u003cURL\u003e` | Compare incoming package names against a top-N-per-ecosystem list (lodash, requests, tokio, Newtonsoft.Json, …) and warn or block close-distance candidates. |\n| **Lifecycle-script gate** (Shai-Hulud-class defence) | `--lifecycle-policy {audit,block,strip}`, `--lifecycle-allow \u003cPKG\u003e` (repeatable), `--lifecycle-strip-on-failure {block,passthrough}`, `--lifecycle-strip-cache-dir \u003cDIR\u003e`, `--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. Same policy also drives the `.vsix` gate: startup-autorun denials carry `x-sakimori-deny: lifecycle-vsix`, bundled-dep known-bad denials carry `lifecycle-vsix-bundled-known-bad`, and in-archive IOC denials carry `lifecycle-vsix-ioc`. See CLAUDE.md Roadmap #15 / #21 / #25 / #26 for the threat models. |\n| **Egress allow-list** | `--network-allow \u003cHOST\u003e` (repeatable), `--network-allow-file \u003cPATH\u003e` | Default-deny hostname filter. Patterns: `host.example.com` (exact) or `*.example.com` (any subdomain, excludes apex). Off by default. |\n| **Install log + advisories** | `--no-install-log`, `--install-log \u003cPATH\u003e` | The local-first append-only audit log feeding `sakimori advisories scan`. On by default at `~/.sakimori/installs.jsonl`. |\n| **OTLP fan-out** | `--otlp-endpoint \u003cURL\u003e`, `--otlp-header \u003cK=V\u003e` (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](#otlp-semantic-conventions) below. |\n| **Custom registries** | `--npm-registry`, `--pypi-registry`, `--pypi-files-host`, `--cargo-registry-host`, `--cargo-sparse-host`, `--nuget-registry` (all repeatable), `--registries-config \u003cFILE\u003e`, `--upstream-ca-file \u003cPATH\u003e` (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](#custom--internal-registries) subsection below. |\n\n**First-run side effect**: generates a self-signed root CA at the\nconfig dir and prints the OS-specific trust command. Subsequent runs\nreuse the existing CA.\n\n**Egress allow-list** closes the eBPF-by-IP gap: when you also run\n`sakimori run` with a network policy, the kernel layer enforces by\nresolved IP and loses against CDN rotation. The proxy's hostname\nfilter sees the SNI / `Host:` value the client actually asked for,\nso an entry like `*.githubusercontent.com` matches every rotating\nCDN IP automatically — the same convention `step-security/harden-runner`\nusers are used to:\n\n```bash\nsakimori proxy start \\\n    --network-allow api.github.com \\\n    --network-allow '*.githubusercontent.com' \\\n    --network-allow registry.npmjs.org\n```\n\n#### Custom / internal registries\n\nThe rewriters + lifecycle gate dispatch by **hostname**. By default\nonly the canonical public hosts are watched — traffic to\n`registry.npmjs.org` runs the npm packument rewriter, traffic to\n`pypi.org` runs the PyPI rewriters, etc. Internal mirrors /\nreplacement registries (Verdaccio, GitHub Packages, Artifactory,\nTakumi Guard, JFrog, Nexus, …) are passed through opaquely unless\nyou teach the proxy about them.\n\nThree layered sources, applied in order with case-insensitive\ndedupe: built-in defaults → optional TOML config file\n(`--registries-config \u003cFILE\u003e`) → per-ecosystem CLI flags. The\ncanonical public hosts remain watched alongside your additions.\n\n```bash\n# CLI flags (repeatable). Each accepts a bare hostname or a URL —\n# the host part is extracted (https://npm.flatt.tech:8443/path →\n# npm.flatt.tech).\nsakimori proxy start \\\n    --npm-registry npm.flatt.tech \\\n    --npm-registry 'https://npm.corp.internal:8443/' \\\n    --pypi-registry pypi.corp.internal \\\n    --nuget-registry nuget.corp.internal\n```\n\n| flag | feeds | canonical default |\n|---|---|---|\n| `--npm-registry`        | npm packument + tarball             | `registry.npmjs.org`        |\n| `--pypi-registry`       | PyPI Warehouse JSON + Simple index  | `pypi.org`                  |\n| `--pypi-files-host`     | PyPI sdist + wheel downloads        | `files.pythonhosted.org`    |\n| `--cargo-registry-host` | crates.io API `/api/v1/crates/…`    | `crates.io`                 |\n| `--cargo-sparse-host`   | crates.io sparse index              | `index.crates.io`           |\n| `--nuget-registry`      | NuGet registration + flat-container | `api.nuget.org`             |\n\nOr pin the same lists in a config file (so a team can ship one\ncanonical config and not paste the same flags into every\ninvocation):\n\n```toml\n# ~/.config/sakimori/registries.toml\n[registries]\nnpm           = [\"registry.npmjs.org\", \"npm.flatt.tech\"]\npypi_index    = [\"pypi.org\"]\npypi_files    = [\"files.pythonhosted.org\"]\ncrates        = [\"crates.io\"]\ncrates_sparse = [\"index.crates.io\"]\nnuget         = [\"api.nuget.org\"]\n```\n\n```bash\nsakimori proxy start --registries-config ~/.config/sakimori/registries.toml\n```\n\nTo lock the proxy to *only* the internal mirrors and reject the\ncanonical public hosts, combine with `--network-allow`:\n\n```bash\nsakimori proxy start \\\n    --registries-config /etc/sakimori/registries.toml \\\n    --network-allow npm.corp.internal \\\n    --network-allow pypi.corp.internal\n```\n\nIf the internal mirror's TLS chain is signed by a **private CA**\nnot in the `webpki-roots`-shipped trust store, pass each CA PEM\nfile with `--upstream-ca-file` (repeatable). Without this the\nupstream handshake fails with `UnknownIssuer` even when the\nhostname is on `--registries-config`:\n\n```bash\nsakimori proxy start \\\n    --registries-config /etc/sakimori/registries.toml \\\n    --upstream-ca-file /etc/ssl/corp-root-ca.pem \\\n    --upstream-ca-file /etc/ssl/intermediate.pem\n```\n\n**Non-goals** (intentionally not done):\n- **Path-shape rewriting.** The custom host must serve the canonical\n  registry's URL shape (npm packument + `/\u003cpkg\u003e/-/\u003cpkg\u003e-\u003cver\u003e.tgz`;\n  PyPI Warehouse JSON / PEP 503/691 Simple; NuGet v3 registration +\n  flat-container; cargo sparse). A mirror that exposes a different\n  layout (e.g. Artifactory at `/artifactory/api/npm/\u003crepo\u003e/`) needs\n  a path-prefix-aware parser variant — not implemented.\n- **`dist.tarball` URL rewriting.** The npm rewriter preserves the\n  upstream's own tarball URL byte-for-byte — mirrors that serve\n  their own tarball URLs keep doing so transparently.\n\n#### OTLP semantic conventions\n\n`--otlp-endpoint` emits one OTLP/HTTP **JSON** `LogRecord` per\nallowed install. Two layers, with different compliance stories:\n\n- **Envelope — OTLP-wire compliant.** The\n  `resourceLogs[].scopeLogs[].logRecords[]` shape, the proto3→JSON\n  name mapping (camelCase wire names; `timeUnixNano` as a decimal\n  string for 64-bit ints), the `AnyValue` variant keys\n  (`stringValue`, `intValue`, …), and the resource attributes\n  `service.name` / `service.version` all follow the OTLP spec.\n  Any spec-compliant collector (otel-collector-contrib, Datadog\n  Agent's OTLP receiver, Honeycomb's OTel endpoint, Loki, …)\n  parses the payload. This is enforced by\n  [`crates/sakimori-proxy/tests/otlp_proto_roundtrip.rs`][otlp-rt],\n  which deserializes every emitted payload through\n  `opentelemetry-proto`'s generated `ExportLogsServiceRequest`\n  type — same strict shape gate a real collector applies.\n\n- **Attribute keys — sakimori-specific, NOT semconv.** The\n  per-install fields use a `package.*` namespace\n  (`package.ecosystem`, `package.name`, `package.version`,\n  `package.resolved_at`, `package.execution_mode`,\n  `package.project_path`, `package.user_agent`, `package.git.*`).\n  OpenTelemetry has no registered \"package install event\"\n  attribute set yet, so we use our own namespace rather than\n  shoehorn the data into `code.*` or `vcs.*`. If/when semconv\n  ships an official equivalent (e.g. `software.package.*`),\n  sakimori will add it alongside the existing keys rather than\n  rename — existing dashboards keep working.\n\nIf you grep your collector config for \"semconv-compliant\nattributes\": **no, these aren't.** If you grep for \"OTLP-wire\ncompatible\": **yes, they are.**\n\n[otlp-rt]: crates/sakimori-proxy/tests/otlp_proto_roundtrip.rs\n\n### `proxy install-ca` / `uninstall-ca`\n\nAdd / remove the root CA from the OS trust store. Cross-platform:\n\n| OS | Mechanism | Privilege prompt |\n|---|---|---|\n| macOS | `security add-trusted-cert -k /Library/Keychains/System.keychain` | `sudo` |\n| Linux | copy to `/usr/local/share/ca-certificates/` + `update-ca-certificates` | `sudo` |\n| Windows | `Import-Certificate -CertStoreLocation Cert:\\LocalMachine\\Root` | UAC via `Start-Process -Verb RunAs` |\n\nIf you're not elevated, sakimori prints the exact shell command\nand exits — no silent reruns with privileges.\n\n```\nsakimori proxy install-ca [--config-dir \u003cPATH\u003e]\nsakimori proxy uninstall-ca [--config-dir \u003cPATH\u003e]\n```\n\n### `proxy install-daemon` / `uninstall-daemon`\n\nWrite a user-level service unit so the proxy runs in the\nbackground at login and restarts on failure.\n\n| OS | Unit | Location |\n|---|---|---|\n| macOS | launchd plist (`KeepAlive`, `RunAtLoad`, `Background` ProcessType) | `~/Library/LaunchAgents/com.sakimori.proxy.plist` |\n| Linux | systemd `--user` unit (`Restart=on-failure`, `WantedBy=default.target`) | `~/.config/systemd/user/sakimori-proxy.service` |\n| Windows | Task Scheduler v1.4 XML (`LogonTrigger`, `RestartOnFailure 99×1m`, `Hidden`) | `%LOCALAPPDATA%\\sakimori\\sakimori-proxy.task.xml` |\n\n```\nsakimori proxy install-daemon [OPTIONS]\n\nOptions:\n  --listen \u003cADDR\u003e         [default: 127.0.0.1:8910]\n  --min-age \u003cDURATION\u003e    [default: 7d]\n  --binary \u003cPATH\u003e         Override the sakimori binary path baked\n                          into the unit. Defaults to the canonical\n                          path of the currently-running executable.\n```\n\nThe command prints the exact activation line (`launchctl bootstrap`\n/ `systemctl --user enable --now` / `schtasks.exe /Create`) — run\nthat to start the service.\n\n### `install-gate`\n\nEdit the user's shell rc file so every new shell exports\n`HTTPS_PROXY` + CA-bundle env vars pointing at the proxy. Idempotent\nvia `# \u003e\u003e\u003e sakimori install-gate \u003e\u003e\u003e` sentinels.\n\n```\nsakimori install-gate shellenv [--listen \u003cADDR\u003e] [--shell {bash,zsh,fish,powershell}]\nsakimori install-gate install  [--rc \u003cPATH\u003e]     [--shell ...]\nsakimori install-gate uninstall [--rc \u003cPATH\u003e]    [--shell ...]\n```\n\nEnvironment variables set (per shell):\n\n| var | who uses it |\n|---|---|\n| `HTTPS_PROXY` / `HTTP_PROXY` (+ lowercase variants) | curl, npm, pip, cargo, dotnet, git |\n| `CARGO_HTTP_CAINFO` | cargo (uses libcurl; doesn't honour system trust store on Linux) |\n| `PIP_CERT` | pip |\n| `NODE_EXTRA_CA_CERTS` | npm, yarn, pnpm |\n| `REQUESTS_CA_BUNDLE` | Python `requests`, poetry, uv |\n| `SSL_CERT_FILE` | generic OpenSSL-using tools |\n\nDefault rc file per shell:\n\n| shell | path |\n|---|---|\n| bash | `~/.bashrc` |\n| zsh  | `~/.zshrc` |\n| fish | `~/.config/fish/config.fish` |\n| powershell | `$PROFILE` = `~/Documents/PowerShell/Microsoft.PowerShell_profile.ps1` |\n\n### `doctor`\n\nOne-command diagnostic. Checks:\n\n1. CA certificate exists + non-empty\n2. CA private key exists + `chmod 600` (Unix)\n3. Proxy is accepting TCP on `--listen`\n4. `$HTTPS_PROXY` in the current shell matches the proxy address\n5. Shell rc file contains the install-gate sentinel\n6. Daemon unit file exists at the expected location\n\nExits `0` on no failures (warnings are informational), `1` otherwise.\n\n```\nsakimori doctor [--listen \u003cADDR\u003e] [--config-dir \u003cPATH\u003e] [--rc \u003cPATH\u003e]\n```\n\nSample output when the proxy is down:\n\n```\n✓ CA certificate               /Users/you/.config/sakimori/ca.pem (644 bytes)\n✓ CA private key               /Users/you/.config/sakimori/ca.key\n✗ Proxy reachable              no listener on 127.0.0.1:8910: Connection refused\n  ↳ start it: `sakimori proxy start` (or, for background: `sakimori proxy install-daemon`)\n! $HTTPS_PROXY                 unset in this shell\n  ↳ run `sakimori install-gate install` and open a new shell\n```\n\n### `deps check`\n\nLockfile-level age gate, usable standalone (no proxy required). Good\nfor a **pre-install CI step** that fails the build before the\nmalicious package is even fetched.\n\n```bash\nsakimori deps check --min-age 7d Cargo.lock package-lock.json\n\n# Different thresholds per ecosystem? Run twice.\nsakimori deps check --min-age 14d Cargo.lock\nsakimori deps check --min-age  3d package-lock.json\n\n# Ignore first-party packages.\nsakimori deps check --min-age 7d --ignore '@my-org/*' package-lock.json\n\n# Machine-readable output for CI gating.\nsakimori deps check --min-age 7d --format json Cargo.lock\n```\n\nSupported lockfile formats:\n\n| ecosystem | lockfile | registry endpoint consulted |\n|---|---|---|\n| cargo | `Cargo.lock` | `crates.io/api/v1/crates/\u003cname\u003e` |\n| npm | `package-lock.json` (lockfileVersion ≥ 2) | `registry.npmjs.org` |\n| pypi | `uv.lock`, `poetry.lock`, `requirements.txt` (exact `==` pins only) | `pypi.org/pypi/\u003cname\u003e/\u003cversion\u003e/json` |\n| nuget | `packages.lock.json` (central-package-management) | `api.nuget.org/v3/registration5-{semver1,gz-semver2}/…` |\n\nExit codes:\n\n| code | meaning |\n|---|---|\n| 0 | all packages meet the threshold |\n| 1 | at least one violation |\n| 2 | parse or I/O error |\n\nCache location: `$XDG_CACHE_HOME/sakimori/deps-cache.json`\n(`%LOCALAPPDATA%\\sakimori\\…` on Windows). Publish dates are\nimmutable, so there's no TTL.\n\n### `deps verify-cache`\n\nRe-hash the package manager's local cache against the lockfile's\n`integrity:` fields and fail if any byte doesn't match what the\nlockfile pinned. This catches the *content* half of the **TanStack\n2025 npm supply-chain attack**: a tarball restored from `actions/\ncache` whose bytes have been swapped, while the lockfile entry\nitself looks untouched.\n\nRun it right after install, in the brief moment when the store is\nfully populated but nothing has built against it yet:\n\n```bash\n# npm cacache (uses ~/.npm/_cacache by default)\nsakimori deps verify-cache --lockfile package-lock.json\n\n# pnpm store v3 (auto-picks ~/.local/share/pnpm/store/v3 on Linux,\n# ~/Library/pnpm/store/v3 on macOS)\nsakimori deps verify-cache --lockfile pnpm-lock.yaml\n\n# cargo registry cache (walks $CARGO_HOME/registry/cache/*/)\nsakimori deps verify-cache --lockfile Cargo.lock\n\n# Override the store path (monorepos with isolated stores, corporate\n# runners with non-standard layouts). Windows defaults are auto-\n# detected (`%LOCALAPPDATA%\\npm-cache\\_cacache`, `%LOCALAPPDATA%\\pnpm\\store\\v3`).\nsakimori deps verify-cache --lockfile pnpm-lock.yaml --cache /opt/pnpm-store/v3\n\n# Machine-readable for CI gating\nsakimori deps verify-cache --lockfile package-lock.json --format json\n```\n\nSupported stores:\n\n| ecosystem | lockfile | store walked |\n|---|---|---|\n| npm | `package-lock.json` (v2/v3) | `~/.npm/_cacache/content-v2/\u003calgo\u003e/\u003caa\u003e/\u003cbb\u003e/\u003crest\u003e` |\n| pnpm | `pnpm-lock.yaml` (v6–v9) | `\u003cstore\u003e/v3/files/\u003caa\u003e/\u003crest\u003e[-exec]` + per-tarball `-index.json` |\n| cargo | `Cargo.lock` | `$CARGO_HOME/registry/cache/\u003creg\u003e/\u003cname\u003e-\u003cversion\u003e.crate` |\n\nExit codes:\n\n| code | meaning |\n|---|---|\n| 0 | every lockfile entry verifies cleanly against the store |\n| 1 | at least one mismatch or missing-from-store entry |\n| 2 | parse / I/O error |\n\n\u003e ⚠️ **Honest limitations.** The pnpm verifier reads the on-disk\n\u003e `\u003crest\u003e-index.json` to find per-file hashes — pnpm discards the\n\u003e tarball after extraction, so a fully coordinated rewrite of both\n\u003e the index and every blob it references would verify clean. The\n\u003e realistic single-file tampering pattern is caught. **pnpm v11+**\n\u003e (the next major after v10 — v10 itself still uses JSON) replaces\n\u003e the per-package JSON index with a single SQLite `index.db` whose\n\u003e BLOB values use msgpackr's non-standard `useRecords: true`\n\u003e extension. v11 stores are not yet supported and `verify-cache`\n\u003e will surface a clear `Unsupported` error rather than silently\n\u003e passing. Workaround until the reader lands: pin pnpm to `\u003c11`.\n\nThe same check is wrapped as a one-line GitHub Actions step —\nsee [CI usage](#ci-usage-github-actions) below.\n\n### `deps watch`\n\nLong-running FS-event watcher for lockfile changes. Designed for\nlaunchd at login.\n\n```bash\n# One-off (Ctrl-C to quit)\nsakimori deps watch ~/code --min-age 7d\n\n# With modal prompts (Keep / Revert via osascript)\nsakimori deps watch ~/code --min-age 7d --action prompt\n\n# Stdout logging, e.g. for tmux / screen\nsakimori deps watch ~/code --min-age 7d --notifier stdout\n```\n\n`--action` controls what happens on violation:\n\n| value | behaviour |\n|---|---|\n| `notify` (default) | Desktop notification. Lockfile untouched, nothing blocked. |\n| `prompt` (macOS only) | Keep / Revert modal via osascript. Revert runs `git checkout HEAD -- \u003clockfile\u003e`. |\n| `revert` | Silently restore the lockfile to `HEAD` via git. Destructive; file must be tracked. |\n\n\u003e ⚠️ **Watch is detection, not prevention.** FS events fire *after*\n\u003e the package manager finishes writing the lockfile — so\n\u003e `preinstall` / `install` / `postinstall` scripts have already\n\u003e run. To actually *prevent* attacks, use the proxy (which sees\n\u003e every fetch) or `deps check` before install.\n\nSee [packaging/macos/README.md](packaging/macos/README.md) for the\nlaunchd plist.\n\n### `workspace snapshot` / `workspace diff`\n\nDetect unexpected file edits made during a build — the supply-chain\nanalogue of \"did this `npm install` rewrite my source files /\n`.git/config` / CI configuration?\". Pure offline; no network.\n\n```bash\n# Before the build\ncoronarium workspace snapshot $GITHUB_WORKSPACE -o /tmp/before.json\n\ncargo build               # …or whatever you actually want to audit\n\n# After the build — exits non-zero on any drift\ncoronarium workspace diff /tmp/before.json $GITHUB_WORKSPACE\n```\n\nWhat the diff reports: files **added**, **modified** (size or\nSHA-256 changed), or **removed** between the two snapshots.\n\nAlways-skipped directory basenames (anywhere in the tree):\n`.git`, `node_modules`, `target`, `dist`, `build`, `vendor`,\n`__pycache__`, `.venv`, `venv`, `.next`, `.turbo`, `.cache`.\nThe list is hardcoded — `.gitignore` is **not** honoured because\nan attacker can write into it. Pass `--skip \u003cname\u003e` (repeatable)\nto extend the list for your own build artefacts.\n\nSymlinks are recorded by target string; the link target is not\ndereferenced. Files larger than 64 MiB default to a size-only\nentry (no SHA), so two oversized files with identical sizes but\ndifferent contents will read as unchanged — bump\n`--max-file-bytes` if that matters for your repo.\n\n`--format json` for machine-readable output. `--allow-drift`\nsuppresses the non-zero exit when you only want the report.\n\n### `extensions snapshot` / `extensions diff`\n\nThe editor-extension counterpart of `workspace snapshot`. Auto-\ndiscovers every editor extension root that exists on the host —\n`~/.vscode/extensions/`, `~/.vscode-insiders/extensions/`,\n`~/.cursor/extensions/`, `~/.windsurf/extensions/`, plus the\nplatform-appropriate VS Code `User/globalStorage/` — and produces\none merged snapshot. Each file's relative path is prefixed with\nthe root's label (`vscode-extensions/foo.bar-1.0.0/package.json`)\nso the same extension id installed under two editors doesn't\ncollide.\n\n```bash\n# Take a baseline (run when you trust the current state)\nsakimori extensions snapshot -o ~/.sakimori/extensions-baseline.json\n\n# Some time later — perhaps after `git pull`, perhaps daily via cron\nsakimori extensions diff ~/.sakimori/extensions-baseline.json\n```\n\nThe diff reports added / modified / removed entries and runs the\nknown-IOC catalog against every implicated path: a sideloaded\n`.vsix` whose `package.json` references `discord.com/api/\nwebhooks/`, or a workspace's `.vscode/tasks.json` configured to\nauto-run on `folderOpen`, surfaces as both a structural drift\nentry and a High-severity IOC hit. High-severity IOC hits force\nexit 1 unconditionally; structural drift exits 1 unless\n`--allow-drift`.\n\nA user without Cursor installed sees no `cursor-extensions/`\nentries — the walker filters to roots that actually exist at\ncall time. `--home \u003cDIR\u003e` overrides `$HOME` for tests / CI.\n\nThis is the **sideload backstop**: even if an attacker bypasses\nthe marketplace fetch entirely (drag-and-drop `.vsix`, `git\nclone` directly into the extensions dir, vendored install), the\ndiff catches the new files. Pair with the proxy's\n`extensionquery` rewriter for the fetch path and you cover both\nthe marketplace-bound and out-of-band install routes.\n\n### `actions audit`\n\nStatic analysis for `.github/workflows/*.yml`. Walks every `uses:`\nin the workflow and flags any reference that isn't pinned to a\n40-char commit SHA — the supply-chain analogue of an unpinned\ndependency. Offline by default; opt into the GitHub API with\n`--resolve` when you want the suggested replacement SHA inline.\n\n```bash\nsakimori actions audit .github/workflows/*.yml\n\n# Machine-readable.\nsakimori actions audit --format json .github/workflows/ci.yml\n\n# Treat first-party (actions/*, github/*) mutable refs as blocking\n# too — useful once you've already pinned all your third-party deps.\nsakimori actions audit --strict .github/workflows/*.yml\n\n# Look up the current SHA each mutable @\u003cref\u003e resolves to via the\n# GitHub REST API. Reads $GITHUB_TOKEN from the env to lift the\n# rate limit from 60/hour to 5000/hour. The output gets a\n# `→ resolved: \u003csha\u003e` line per finding (text) or a `resolved_sha`\n# field (JSON) so you can copy-paste the right pinned form.\nsakimori actions audit --resolve .github/workflows/*.yml\n```\n\nSeverity:\n\n| | when |\n|---|---|\n| **error** | third-party action with mutable tag/branch (`foo/bar@v1`, `foo/bar@main`) |\n| **warn**  | first-party (`actions/*`, `github/*`) mutable tag, or docker image without `@sha256:` digest |\n| **ok**    | 40-char SHA pin, local action (`./...`), docker image with digest |\n\nExit code: `1` when at least one error is present (or any warn,\nunder `--strict`); `0` otherwise. Composite-action `action.yml`\nfiles are ignored — only workflow files (those with a top-level\n`jobs:` block) are walked. Resolution failures (rate-limit, removed\naction) appear as `→ resolve failed: …` per finding without\naborting the audit.\n\n**Workflow-level lint** (in addition to per-`uses:` SHA pinning):\nthe auditor also flags the `pull_request_target` + writable Actions\ncache pattern — the TanStack 2025 cache-poisoning vector. If a\nworkflow runs on `pull_request_target` (or `workflow_run`) **and**\nany job step writes to the GitHub Actions cache, that's an Error.\n\n```bash\nsakimori actions audit .github/workflows/bundle-size.yml\n# .github/workflows/bundle-size.yml  (1 ok, 0 warn, 0 error)\n#   ERROR  [pull_request_target_with_cache_write] workflow runs on\n#          `pull_request_target` and writes to the Actions cache —\n#          an untrusted fork PR can poison the cache that a later\n#          trusted workflow restores (TanStack-style npm supply-chain\n#          compromise). …\n#          · size (actions/cache@v4): actions/cache writes via post-step on cache miss\n```\n\nDetected cache writers: `actions/cache@*`, `actions/cache/save@*`,\n`actions/setup-{node,python,java,dotnet,ruby}` with `with.cache:`,\n`actions/setup-go` (caches by default), `Swatinem/rust-cache`,\n`mozilla-actions/sccache-action`, `astral-sh/setup-uv` with\n`enable-cache: true`. Cache writes use a runner-internal token, not\nthe workflow `GITHUB_TOKEN`, so `permissions: contents: read` does\n**not** block them. Split cache-writing steps into a separate\nworkflow that doesn't run on fork PRs, or gate the offending job\nbehind `if: github.event.pull_request.head.repo.full_name ==\ngithub.repository`. JSON output puts these under a top-level\n`workflow_findings` array alongside the per-`uses:` `findings`.\n\n### `run`\n\nWraps a command under eBPF (Linux) / ETW (Windows) supervision and\nobserves — optionally denies — its syscalls:\n\n- `connect(2)` on IPv4 / IPv6\n- `openat(2)`\n- `execve(2)`\n\n```bash\nsakimori run \\\n  --policy .github/sakimori.yml \\\n  --mode audit \\\n  --log sakimori.log.json \\\n  --html sakimori-report.html \\\n  -- cargo test\n```\n\nFlags:\n\n| flag | env | default | description |\n|---|---|---|---|\n| `--policy` / `-p` | `SAKIMORI_POLICY` | — | policy file (YAML or JSON) |\n| `--mode` | — | from policy | `audit` or `block` — overrides the policy's `mode:` |\n| `--log` | — | `-` (stdout) | JSON audit log destination |\n| `--summary` | `GITHUB_STEP_SUMMARY` | — | markdown summary |\n| `--html` | — | — | self-contained HTML report (dark-mode aware, filterable) |\n| `--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 |\n| `--snapshot-skip` | — | — | extra dir basenames to skip during the snapshot (repeatable) |\n| `--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 |\n\nExit code: child's exit code, unless `mode=block` and **either**:\n- at least one event was denied, **or**\n- a `--snapshot-workspace` baseline was taken and the post-run diff is non-empty\n\n→ exits `1` either way.\n\nPolicy format:\n\n```yaml\n# .github/sakimori.yml\nmode: block                    # audit | block\n\nnetwork:\n  # default is `deny`, so only listed destinations can be reached.\n  allow:\n    - target: api.github.com   # A+AAAA resolved at startup\n      ports: [443]\n    - target: 140.82.112.0/20  # CIDR expanded (up to /16 for v4)\n      ports: [22, 443]\n    - target: 2606:4700::/48   # IPv6 CIDRs work too\n      ports: [443]\n\nfile:\n  default: allow               # most builds open hundreds of files\n  deny:\n    - /etc/shadow\n    - /root/.ssh\n\nprocess:\n  deny_exec:\n    - /usr/bin/nc\n\nenv:\n  # Scrub the env block before the child execs. Real prevention,\n  # not a tripwire — `Command::env_clear()` happens before\n  # `execve`, so the child (and its postinstall grandchildren)\n  # literally cannot read what's been stripped.\n  default: pass                  # `pass` keeps everything not on `deny`;\n                                 # `clear` flips it to allowlist mode\n  allow: [PATH, HOME, \"GITHUB_*\"]\n  deny: [\"AWS_*\", \"*_TOKEN\", \"*_SECRET\", NPM_TOKEN]\n```\n\n**First-time setup pattern** — run in `mode: audit` once, then let\n`policy suggest` turn the log into a starter policy, prune by hand,\nand flip to `mode: block`:\n\n```bash\ncoronarium run --mode audit --log audit.json -- cargo test\ncoronarium policy suggest audit.json -o .github/coronarium.yml\n$EDITOR .github/coronarium.yml      # remove anything you don't want allowed\ncoronarium run -p .github/coronarium.yml --mode block -- cargo test\n```\n\n`suggest` populates `network.allow` (one entry per host:port observed,\nhostnames preferred over raw IPs) and `file.allow` (one entry per\nparent directory observed). Exec targets are surfaced as a\ncommented `# observed_exec:` block — `process.deny_exec` is\ndeliberately left empty because the suggester can't know which of\nthe binaries the build actually wanted.\n\n**Curated rule packs (`policy preset`):** ready-to-merge YAML blocks\nfor known supply-chain attack patterns. Currently shipped:\n\n- `sakimori policy preset persistence` — `file.deny` tripwire for\n  OS-level persistence writes (launchd / systemd / cron / shell rc\n  / `~/.ssh`). Per-user paths expand from `$HOME` (override with\n  `--home /path`); system paths always included.\n- `sakimori policy preset cloud-secret-egress` — `network.deny`\n  tripwire for AWS / GCP / Azure IMDS and STS-style secret\n  endpoints. Pairs with `sakimori proxy start --network-allow ...`\n  for SNI-level enforcement.\n\nBoth presets print to stdout (or `-o policy.yml`) with explanatory\ncomment headers so the operator can pick the entries that fit their\nthreat model and merge into an existing policy. The persistence\npreset ships in `mode: audit` because its full list exceeds the\nLinux 8-entry kernel cap on `file.deny` under `mode: block`; to\nenforce, prune to your 8 most critical paths and flip the `mode:`\nfield to `block`. The cloud-secret-egress preset ships in\n`mode: block` (no cap on `network.deny`).\n\n**Known-IOC workspace scan (`workspace scan-iocs`):** walk a\nworkspace and flag files whose path / basename / content matches\na known supply-chain compromise fingerprint (e.g. `.claude/\nsetup.mjs` dropped by the Shai-Hulud npm worm; basename `.npmrc`\nfor token-exfil; content needles for `webhook.site`,\n`discord.com/api/webhooks/`, `requestbin.com`). Distinct from\n`workspace diff` — diff catches \"something changed during the\nbuild,\" scan-iocs catches \"this file exists at all, which it\nshouldn't.\" The catalog is bundled in the binary (versioned;\n`CATALOG_VERSION` in `sakimori-core::iocs`). Exits non-zero on\nany High-severity hit; `--strict` escalates Medium-severity hits\nto exit 1 too. Same skip list as `workspace snapshot` (`.git`,\n`node_modules`, `target`, …); extend with `--skip \u003cNAME\u003e`.\n\n```bash\nsakimori workspace scan-iocs $GITHUB_WORKSPACE\nsakimori workspace scan-iocs . --format json\nsakimori workspace scan-iocs . --strict --skip my-build-artefact\n```\n\n`scan-iocs` is also wired into `workspace diff`, `sakimori run\n--snapshot-workspace`, and `sakimori daemon start\n--workspace-baseline …` automatically — every added / modified\npath in the drift report is scanned against the same catalog and\nthe findings land in the JSON log under `workspace_iocs`. A\nHigh-severity hit forces exit 1 in any mode (Audit too); `--allow-\ndrift` does not suppress it.\n\nThe bundled catalog is the only source today. A signed-YAML\nrefresh path (`sakimori iocs update`) is a roadmap item — see\nCLAUDE.md Roadmap #18 for the planned surface.\n\nThe HTML report includes:\n- verdict (ALLOW / DENY), kind, pid, comm\n- **host column** (PTR-resolved reverse DNS for connect events)\n- detail (IP:port / filename / exec argv)\n- filter box matching across all fields\n- dark-mode aware, self-contained (no external CSS/JS)\n\n**Per-event source attribution (Linux):** the supervisor walks\n`/proc/\u003cpid\u003e/{status,cmdline}` PPid chains at event time and tags\neach event with the originating package manager (npm, pnpm, yarn,\ncargo, pip, uv, poetry, dotnet, go, maven, gradle, bundler,\ncomposer). That shows up as a `source: { package_manager, root_argv,\nchain }` field on every JSON-log event and as a \"Sources\" top-N\ntable in the step summary, so a connect to `evil.example` reads as\n\"came from `npm install foo@1.2.3`\" rather than just \"from pid\n12345 (sh)\". Best-effort — pids that have already exited by the\ntime the userspace drain reads the ringbuf get `source: null` and\nfall into the `(unattributed)` row. Windows ETW supervisor doesn't\nattach attribution yet.\n\n---\n\n## CI usage (GitHub Actions)\n\n### Minimal: run every install through the proxy\n\nWorks on **Linux, macOS, and Windows** GitHub-hosted runners (Windows\nrequires sakimori v0.34.3 or newer — earlier Windows release tarballs\nship only `sakimori-win.exe`, the ETW supervisor, which has no proxy\nsubcommand). The proxy starts in the background as the action's main\nstep, exports `HTTPS_PROXY` + the CA bundle for every common HTTPS\nclient via `$GITHUB_ENV`, and survives across `run:` step boundaries\nuntil the post-step kills it at end-of-job.\n\n```yaml\njobs:\n  build:\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macos-latest, windows-latest]\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v4\n\n      # Spawns `sakimori proxy start` detached and appends\n      # HTTPS_PROXY / CARGO_HTTP_CAINFO / NODE_EXTRA_CA_CERTS /\n      # PIP_CERT / REQUESTS_CA_BUNDLE / SSL_CERT_FILE to $GITHUB_ENV\n      # for every step after this one.\n      - uses: bokuweb/sakimori/proxy@v0\n        with:\n          min-age: 7d\n\n      - run: npm ci          # routed through the proxy\n      - run: cargo test      # routed through the proxy\n      - run: pip install -r requirements.txt   # routed through the proxy\n```\n\nInputs:\n\n| input | default | description |\n|---|---|---|\n| `min-age` | `7d` | Minimum package age. Same grammar as `--min-age`. |\n| `listen` | `127.0.0.1:8910` | Proxy listen address. |\n| `fail-on-missing` | `false` | Treat unknown publish dates as deny. |\n| `version` | `v0` | sakimori release tag to download. |\n| `token` | `${{ github.token }}` | Used by `gh release download`. |\n\nOutputs:\n\n| output | description |\n|---|---|\n| `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`. |\n\n### Alternative: lockfile-only pre-flight check\n\nCheaper (no proxy), but fails loudly on any too-young dep instead\nof silently falling back.\n\n```yaml\n- uses: bokuweb/sakimori@v0\n- run: $SAKIMORI_BIN deps check --min-age 7d Cargo.lock package-lock.json\n- run: cargo test   # only reached if the check passed\n```\n\n### Cache-poisoning guard: `bokuweb/sakimori/verify-cache@v0`\n\nThe proxy filters at **fetch** time — it can't see bytes restored\nfrom `actions/cache`. If your workflow uses `actions/cache` (or\n`actions/setup-node` with `cache:`, `Swatinem/rust-cache`, etc.) a\npoisoned restore happens between cache-restore and install, behind\nthe proxy's back.\n\nDrop this step in **right after install** to re-hash every blob in\nthe local store against the lockfile's `integrity:` fields:\n\n```yaml\n- uses: bokuweb/sakimori/proxy@v0\n  with: { min-age: 7d }\n\n- uses: actions/cache@v4\n  with: { path: ~/.local/share/pnpm/store, key: ... }\n- run: pnpm install        # populates / hits the cache\n\n# ↓ catches TanStack-style cache poisoning: cache restored a\n# tarball whose bytes don't match what the lockfile pinned.\n- uses: bokuweb/sakimori/verify-cache@v0\n  with:\n    lockfile: pnpm-lock.yaml\n```\n\nSupports `package-lock.json`, `pnpm-lock.yaml`, and `Cargo.lock`;\nauto-picks the cache root for the runner OS. Inputs:\n\n| input | default | description |\n|---|---|---|\n| `lockfile` | (required) | Path to `package-lock.json`, `pnpm-lock.yaml`, or `Cargo.lock`. |\n| `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. |\n| `format` | `text` | `text` or `json`. |\n| `version` | `v0` | sakimori release tag. |\n| `token` | `${{ github.token }}` | Used by `gh release download`. |\n\nExit codes match the CLI: `0` clean, `1` on any mismatch / missing\nentry. **pnpm v11+ SQLite stores are not yet supported** — the\naction exits with a clear `Unsupported` error rather than passing\nsilently. (v10 still uses the JSON layout and works fine.)\n\n### eBPF-supervised test run — job-scoped form (Linux only)\n\nUse `bokuweb/sakimori/job@v0` when you want a single audit log covering\n**every step in the job** instead of just one wrapped command. The\naction's pre-hook spawns a background eBPF supervisor attached to the\nrunner-worker's cgroup; cgroup v2 inheritance means every step the\nrunner forks afterwards (`actions/checkout`, your `run:` blocks,\n`actions/upload-artifact`, ...) is observed by the same supervisor.\nThe post-hook flushes the JSON log / step summary / HTML report and\nfails the job if `mode: block` denied anything.\n\n```yaml\nruns-on: ubuntu-latest\nsteps:\n  - uses: bokuweb/sakimori/job@v0   # MUST come before checkout so the\n    with:                           # supervisor is up first\n      policy: .github/sakimori.yml\n      mode: block\n      html: sakimori-report.html\n\n  - uses: actions/checkout@v4\n  - run: corepack enable\n  - run: pnpm install --frozen-lockfile\n  - run: pnpm build\n  - run: pnpm test\n  # post-hook of bokuweb/sakimori/job runs here automatically\n```\n\nLimitations: Linux runners only (Windows needs a different kernel\nhook), and **container jobs** (`jobs.\u003cid\u003e.container:`) are unsupported\nbecause the host-side cgroup attach can't reach steps that run inside\nthe container. Matrix shards and reusable-workflow callers are each\ntheir own job and need their own `bokuweb/sakimori/job@v0`.\n\n**Uploading the audit log from the same job**: the daemon writes\nits JSON / HTML / step-summary at end-of-job (the post-hook), which\nis too late for an `actions/upload-artifact` step inside the same\njob. Drop in `bokuweb/sakimori/job/stop@v0` right before the\nupload to flush the daemon early:\n\n```yaml\n- uses: bokuweb/sakimori/job@v0\n  with: { policy: .github/sakimori.yml, mode: block }\n\n- uses: actions/checkout@v4\n- run: pnpm test\n\n- uses: bokuweb/sakimori/job/stop@v0       # flush + stop\n- uses: actions/upload-artifact@v4\n  with:\n    name: sakimori-report\n    path: |\n      sakimori.log.json\n      sakimori-report.html\n```\n\nIt's idempotent — the daemon's own post-hook turns into a no-op on\nthe missing pid-file. On non-Linux matrix entries the sub-action\nno-ops silently, so it's safe to drop into a cross-OS workflow.\n\n**Tamper detection**: pass `snapshot-workspace: \u003cDIR\u003e` to also catch\non-disk tampering. The daemon can't take the baseline itself (it\nstarts before checkout), so add a tiny step right after checkout that\nrecords the baseline — the action exports the paths for you:\n\n```yaml\n- uses: bokuweb/sakimori/job@v0\n  with:\n    policy: .github/sakimori.yml\n    mode: block\n    snapshot-workspace: .\n\n- uses: actions/checkout@v4\n- run: sudo -E \"$SAKIMORI_BIN\" workspace snapshot\n       \"$SAKIMORI_WORKSPACE_DIR\" -o \"$SAKIMORI_BASELINE_PATH\"\n- run: pnpm install --frozen-lockfile\n- run: pnpm build\n```\n\nThe daemon re-snapshots `$SAKIMORI_WORKSPACE_DIR` at post-time, diffs\nagainst the baseline, and surfaces drift in the JSON log + step\nsummary. Forgetting the snapshot step is non-fatal (the daemon logs a\nwarning and the drift section is omitted).\n\n### eBPF-supervised test run — one-step form (Linux + Windows)\n\nThe simplest form: pass the command you want supervised via the\n`run:` input. The action installs sakimori AND wraps the command\nwith `sakimori run` for you — no separate `sudo -E env \"PATH=$PATH\"\n\"$SAKIMORI_BIN\" run …` step required.\n\n```yaml\nstrategy:\n  matrix:\n    os: [ubuntu-latest, windows-latest]\nruns-on: ${{ matrix.os }}\nsteps:\n  - uses: actions/checkout@v4\n  - uses: bokuweb/sakimori@v0\n    with:\n      policy: .github/sakimori.yml\n      mode: audit\n      html: sakimori-report.html\n      run: |\n        corepack enable\n        cargo test\n        pnpm install --frozen-lockfile\n        pnpm test\n```\n\nOn Linux the script runs under\n`sudo -E env \"PATH=$PATH\" \"$SAKIMORI_BIN\" run … -- bash -euxo pipefail -c '\u003crun\u003e'`;\non Windows under `\u0026 $env:SAKIMORI_BIN … -- pwsh -NoProfile -Command \"\u003crun\u003e\"`.\n`--summary` defaults to `$GITHUB_STEP_SUMMARY` and `--log` defaults\nto the `log:` input (`sakimori.log.json`). Add `snapshot-workspace:\n\u003cdir\u003e` to also catch on-disk tampering.\n\n### eBPF-supervised test run — explicit form (Linux + Windows)\n\nIf you need more control over the wrapper invocation, omit `run:`\nand write the `sakimori run` step yourself. The action exports\n`$SAKIMORI_BIN`, `$SAKIMORI_POLICY`, `$SAKIMORI_MODE`, and\n`$SAKIMORI_LOG` for you.\n\n```yaml\nstrategy:\n  matrix:\n    os: [ubuntu-latest, windows-latest]\nruns-on: ${{ matrix.os }}\nsteps:\n  - uses: actions/checkout@v4\n  - uses: bokuweb/sakimori@v0\n    with:\n      policy: .github/sakimori.yml\n      mode: audit\n\n  - if: runner.os == 'Linux'\n    run: |\n      # `sudo -E` preserves env *except* PATH (sudo always replaces\n      # it with secure_path). `env \"PATH=$PATH\"` re-injects the\n      # runner user's PATH so the supervised child can find tools\n      # installed outside /usr/bin (pnpm, cargo, rustup toolchains).\n      sudo -E env \"PATH=$PATH\" \"$SAKIMORI_BIN\" run \\\n        --policy  \"$SAKIMORI_POLICY\" \\\n        --mode    \"$SAKIMORI_MODE\" \\\n        --log     \"$SAKIMORI_LOG\" \\\n        --html    sakimori-report.html \\\n        --summary \"$GITHUB_STEP_SUMMARY\" \\\n        -- cargo test\n\n  - if: runner.os == 'Windows'\n    shell: pwsh\n    run: |\n      \u0026 $env:SAKIMORI_BIN `\n        --policy $env:SAKIMORI_POLICY `\n        --log    sakimori.log.json `\n        --html   sakimori-report.html `\n        -- cargo test\n\n  - uses: actions/upload-artifact@v4\n    if: always()\n    with:\n      name: sakimori-report-${{ runner.os }}\n      path: |\n        sakimori-report.html\n        sakimori.log.json\n```\n\n### PR comment with the HTML report\n\n`bokuweb/sakimori/comment@v0` reads the JSON log and upserts a\nsingle PR comment (keyed by an HTML marker, re-runs edit in place).\nEmbeds a `gh run download` one-liner to view the full HTML on your\nmachine.\n\n```yaml\n- uses: bokuweb/sakimori/comment@v0\n  if: github.event_name == 'pull_request'\n  with:\n    log: sakimori.log.json\n    artifact-name: sakimori-report\n    html-filename: sakimori-report.html\n    # fail-on-denied: \"true\"                # optional\n```\n\n### Runner support matrix\n\n| runner | proxy | supervised run | notes |\n|---|---|---|---|\n| `ubuntu-latest`, `ubuntu-22.04`, `ubuntu-24.04` | ✅ | ✅ | canonical Linux target, eBPF + tracepoints |\n| `ubuntu-24.04-arm` | ✅ | ✅ | aarch64 binary ships in each release |\n| `windows-latest` | ✅ | ✅ | ETW public providers; elevated by default |\n| `windows-2022`, `windows-2019` | ✅ | ⚠️ | probably works but not smoke-tested |\n| `macos-latest` | ✅ | ❌ | supervised mode is Linux/Windows only |\n| container jobs (`container:` on Linux) | ✅ | ⚠️ | needs `--privileged` + host cgroup mount |\n| self-hosted Linux | ✅ | ⚠️ | needs passwordless sudo, kernel ≥ 5.13 |\n| self-hosted Windows | ✅ | ⚠️ | needs Administrator for ETW |\n\n---\n\n## Docker image\n\nPrebuilt multi-arch image on GHCR:\n\n```bash\ndocker pull ghcr.io/bokuweb/sakimori-proxy:v0\n```\n\nTags: `v0` (floating), `v0.N`, `v0.N.M`, `latest`. Available archs:\n`linux/amd64`, `linux/arm64`.\n\nRun with a named volume so the CA persists across restarts:\n\n```bash\ndocker run --rm -p 8910:8910 \\\n    -v sakimori-conf:/etc/sakimori-xdg \\\n    ghcr.io/bokuweb/sakimori-proxy:v0 \\\n    --listen 0.0.0.0:8910 --min-age 7d\n\n# One-shot: grab the generated CA so hosts can trust it.\ndocker run --rm -v sakimori-conf:/etc/sakimori-xdg \\\n    --entrypoint cat ghcr.io/bokuweb/sakimori-proxy:v0 \\\n    /etc/sakimori-xdg/sakimori/ca.pem \u003e /tmp/sakimori-ca.pem\n```\n\nThen on each host:\n\n```bash\nexport HTTPS_PROXY=http://\u003ccontainer-host\u003e:8910\nexport CARGO_HTTP_CAINFO=/tmp/sakimori-ca.pem\n# (or install-ca into your OS trust store with the CA you just copied)\n```\n\n---\n\n## Configuration reference\n\n### Duration grammar (`--min-age`, policy `age`)\n\nInteger + unit. Bare numbers default to days.\n\n| suffix | unit |\n|---|---|\n| `d` | days |\n| `h` | hours |\n| `m` | minutes |\n| `s` | seconds |\n\nExamples: `7d`, `72h`, `30m`, `3600s`, `7` (= 7 days).\n\n### File locations\n\n| OS | CA + key | Cache | Daemon unit |\n|---|---|---|---|\n| macOS | `~/.config/sakimori/ca.{pem,key}` (or `$XDG_CONFIG_HOME`) | `~/Library/Caches/sakimori/deps-cache.json` | `~/Library/LaunchAgents/com.sakimori.proxy.plist` |\n| Linux | `$XDG_CONFIG_HOME/sakimori/ca.{pem,key}` | `$XDG_CACHE_HOME/sakimori/deps-cache.json` | `~/.config/systemd/user/sakimori-proxy.service` |\n| Windows | `%LOCALAPPDATA%\\sakimori\\ca.{pem,key}` | `%LOCALAPPDATA%\\sakimori\\deps-cache.json` | `%LOCALAPPDATA%\\sakimori\\sakimori-proxy.task.xml` |\n\n### Environment variables read\n\n| var | purpose |\n|---|---|\n| `SAKIMORI_POLICY` | Default policy file for `run` / `check-policy` |\n| `SAKIMORI_MODE` | Override policy `mode` in `run` |\n| `SAKIMORI_LOG` | Default log destination in `run` |\n| `SAKIMORI_BIN` | Set by the GH Action install step |\n| `SAKIMORI_BPF_OBJ` | Path to `sakimori.bpf.o` (Linux only) |\n| `GITHUB_STEP_SUMMARY` | Default `--summary` target |\n| `XDG_CONFIG_HOME` / `XDG_CACHE_HOME` | Override default config/cache dir |\n\n---\n\n## Troubleshooting\n\n### `sakimori doctor` says the proxy is unreachable\n\n- Check it's actually running: `pgrep -f 'sakimori proxy'`\n- On macOS: `launchctl list | grep sakimori`\n- On Linux: `systemctl --user status sakimori-proxy`\n- On Windows: `schtasks /Query /TN sakimori-proxy`\n- Try `sakimori proxy start` in the foreground — see the log.\n\n### TLS errors from cargo / npm / pip\n\nCargo on Linux uses libcurl which doesn't read the system trust\nstore — `CARGO_HTTP_CAINFO` must point at the sakimori CA.\nLikewise `PIP_CERT` for pip and `NODE_EXTRA_CA_CERTS` for npm.\n\n`install-gate install` sets all of these. If you skipped that,\neither install-gate now or set them manually.\n\n### `install-ca` on macOS says \"needs privilege\"\n\nmacOS keychain writes need `sudo`. Re-run with sudo, or copy the\nprinted `security add-trusted-cert …` line and run it yourself.\n\n### `npm install` still pulls a too-young version\n\n1. Is the proxy running? `sakimori doctor`\n2. Is `HTTPS_PROXY` set in **this** shell? (install-gate only\n   applies to new shells.) `echo $HTTPS_PROXY`\n3. Is the package being downloaded from a host sakimori\n   intercepts? By default only the canonical public hosts\n   (`registry.npmjs.org`, `pypi.org`, `files.pythonhosted.org`,\n   `crates.io`, `index.crates.io`, `api.nuget.org`) are watched.\n   Internal mirrors / replacement registries need to be added —\n   see [Custom / internal registries](#custom--internal-registries)\n   for the `--npm-registry` / `--registries-config` flags.\n\n### Container / remote Docker usage\n\nRun the proxy on a separate host and point client env at it:\n\n```bash\nexport HTTPS_PROXY=http://proxy.corp.internal:8910\nexport CARGO_HTTP_CAINFO=/etc/sakimori/ca.pem  # copy from the proxy container\n```\n\n---\n\n## Known limitations\n\nHonest assessment. Full details in [CLAUDE.md](CLAUDE.md).\n\n### Proxy\n\n\u003c!-- pypi HTML Simple index (PEP 503) and nuget flat-container are\n     now silently filtered via out-of-band JSON-API / registration\n     lookups (cached in-proxy for 10 min). No limitation to document\n     here anymore. --\u003e\n- **Sigstore bundle verification** (not just claim presence) is a\n  roadmap item. `--require-provenance` currently checks that the\n  `dist.attestations.provenance.predicateType` field is non-empty,\n  which is already meaningful (npm refuses to attach it unless\n  the publish came from OIDC-authenticated CI) but the bundle\n  itself isn't cryptographically verified.\n- **CDN IP rotation across long runs**: handled by\n  `sakimori run --dns-refresh-interval \u003csecs\u003e`, which re-resolves\n  `network.allow` / `network.deny` hostnames every N seconds and\n  additively inserts new IPs into the eBPF maps. Default `15`\n  (seconds); set `0` to disable, raise to 60–300 for very long\n  CI jobs behind round-robin DNS. Entries are never removed once\n  written, so increasing the rate is safe and won't kill active\n  connections.\n\n### Linux supervised run\n\n- **Network block works at the kernel** (EPERM from\n  cgroup/connect4|6).\n- **File block is \"tripwire\"** — `bpf_send_signal(SIGKILL)` on a\n  matching `openat`. The fd may briefly exist; the process dies\n  before consuming it. For a truly pre-open block we'd need\n  `bpf_override_return`, which is CONFIG_BPF_KPROBE_OVERRIDE\n  dependent (roadmap).\n- **Exec deny is audit-only** — events get `denied: true` in the\n  log and block-mode exits non-zero, but the exec itself happens.\n  Same roadmap item as file block.\n- **deps watch** is detection, not prevention — FS events fire\n  after the package manager has already run `postinstall`.\n\n### Windows supervised run\n\n- `network.default: deny` is **audit-only** — Windows Defender\n  Firewall evaluates block over allow, so an allowlist pattern\n  would require flipping the system-wide default-outbound to\n  Block, which we won't do silently. `network.deny: […]` is\n  kernel-enforced.\n\n### macOS\n\n- No supervised run mode. `run` is Linux/Windows only — on macOS\n  sakimori is a desktop-level tool (proxy + deps + watch).\n\n---\n\n## Development\n\n```bash\n# Full test suite (core + proxy + install-gate + daemon + doctor)\ncargo test --workspace\n\n# Lint\ncargo clippy --workspace --all-targets -- -D warnings\ncargo fmt --all -- --check\n\n# Build the eBPF object (Linux only, requires nightly + bpf-linker)\nrustup toolchain install nightly --component rust-src\ncargo install bpf-linker\ncd crates/sakimori-ebpf\nRUSTUP_TOOLCHAIN=nightly cargo build --release \\\n    --target bpfel-unknown-none -Z build-std=core\n```\n\nCrates:\n\n- `sakimori-common` — `no_std` POD types shared with eBPF (ring\n  buffer records, map keys)\n- `sakimori-core` — platform-neutral: events, policy, matcher,\n  stats, HTML report, `deps::*`, watch\n- `sakimori-ebpf` — Linux kernel programs (cgroup/connect\n  tracepoints). Excluded from the main workspace.\n- `sakimori-proxy` — HTTPS MITM proxy (hudsucker + rustls),\n  registry parsers, rewriters (crates/npm/pypi/nuget), CA\n  management, daemon unit generators\n- `sakimori` — userspace CLI and Linux supervisor\n- `sakimori-win` — Windows ETW supervisor + Defender Firewall\n  integration (separate workspace for dep isolation)\n\nArchitecture notes live in [CLAUDE.md](CLAUDE.md).\n\n---\n\n## Commercial support\n\nsakimori is free to use under MIT/Apache-2.0. If your team needs\nany of the following, the maintainer offers paid engagements:\n\n- Onboarding help (writing/auditing your `policy.yml`, integrating\n  with your CI, tuning per-runner thresholds).\n- Priority bug fixes and feature requests.\n- Private Slack/Discord channel for questions.\n- Custom ecosystem support or proprietary registry adapters.\n\nContact: **bokuweb12@gmail.com**\n\nFor non-commercial appreciation, [GitHub Sponsors](https://github.com/sponsors/bokuweb)\nis also welcome.\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md). All commits must be signed off\n([DCO](https://developercertificate.org/)) — `git commit -s`.\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbokuweb%2Fsakimori","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbokuweb%2Fsakimori","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbokuweb%2Fsakimori/lists"}