Run Sato Hub Preflight over your dependency manifest on every push. It prints what Sato Hub has on record for each name — is it a listing we track, is it active, when was it last checked — as a table in the job summary and as annotations on the manifest lines.
- uses: satohubai/preflight-action@v1
with:
manifest: package.jsonNo build step, no dependencies, no token. One HTTPS call.
A Preflight verdict is a description of evidence. Each result names what was checked, which field it was read from, and when that field was written.
| verdict | what it means | |
|---|---|---|
| 🟢 | go |
The listing is active, scores in the High Sato Score tier, and carries at least one earned check — a reproduced install, a fresh successful daily probe, or a sustained observed success rate. |
| 🟡 | caution |
Something resolved, but it did not clear the go bar. Most of the index sits here, because most projects have never been independently checked. |
| 🔴 | no |
The listing is retired, a probed endpoint did not answer, or the observed record is mostly failure attributed to the host. |
| ⚪ | unknown |
Sato Hub holds no record of this target. |
This is not a vulnerability scanner. It is not a security review, not a
licence checker, not malware detection, and not an opinion about whether a
package is safe to install. go does not mean safe. no does not mean
dangerous. If you want CVE scanning, run a CVE scanner — this answers a
different question.
unknown never fails a build, at any fail_on setting. A package Sato Hub
has never heard of is unknown, which is a fact about our index rather than about
your dependency. An action that could redden your build over a name we simply do
not index would not deserve to stay in your workflow.
Your manifest is never turned into traffic. Package names are parsed and
looked up against records Sato Hub already holds; nothing in your manifest is
fetched, resolved or contacted. Only the endpoints and agents inputs — which
you type on purpose — reach a live probe.
| input | default | what it does |
|---|---|---|
mode |
preflight |
preflight or template-drift (below). |
fail_on_error |
false |
template-drift only: exit non-zero when the drift endpoint or GitHub API cannot be read. |
manifest |
package.json |
Path to the manifest. package.json or requirements.txt. false to skip. |
manifest-kind |
inferred | Force package.json or requirements.txt. |
packages |
— | Extra package names, newline- or comma-separated. |
repos |
— | GitHub repositories (owner/name or URL). |
endpoints |
— | https MCP endpoints. The only input that causes a live probe. |
agents |
— | ERC-8004 references, <chain>:<id> (e.g. base:42). |
fail_on |
no |
no · caution · none. Which verdict exits non-zero. |
annotations |
true |
Emit annotations on the manifest lines. |
api |
https://satohub.ai |
API base URL. |
custody |
true |
On pull_request, run Sato Check diff mode (below). false to skip. |
github-token |
${{ github.token }} |
Posts one sticky PR comment. Needs pull-requests: write; without it the report goes to the job summary only. |
fail-on |
— | Alias for fail_on. Both accept a comma list; add key_egress_observed to fail when Sato Check saw a planted key leave. |
On a pull request, the action reads only what the PR adds or changes:
package.jsondependencies/devDependenciesadded or re-versioned (against the base branch),mcpServersblocks added or changed in.mcp.jsonorclaude_desktop_config.json,SKILL.mdfiles underskills/or.claude/skills/(listed as changed; a local skill file is not a registry install, so there is no record to read for it).
It sends them as one install command / config to POST https://satohub.ai/api/check/install
and writes the four answers per dependency — Does it take your key? Does your key
leave? Can it move funds on its own? What changed? — to the job summary and to one
sticky PR comment that is updated on every push.
A profile describes what Sato Hub read and ran, with dates. It is not a safety rating, an audit or an endorsement, and "not found" is not "not there".
It fails open. An unreachable API, a shallow clone, a missing token or an
answer of unknown is reported and never fails the job. The only failure is the
one you opt into: fail_on: no,key_egress_observed fails when a planted key was
seen leaving during one of Sato Hub's runs for a dependency the PR brings in.
name: preflight
on: [pull_request]
permissions:
contents: read
pull-requests: write # for the sticky comment; optional
jobs:
preflight:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # diff mode compares against the base branch
- uses: satohubai/preflight-action@v1
with:
manifest: package.json
fail_on: none,key_egress_observedUp to 50 targets per run. Over the cap the API refuses rather than truncating — a clean summary over a silent subset would be believed.
Commit a .sato/policy.json to the repository being checked and the diff mode
above also evaluates each changed dependency's Sato Check readings against it.
The job fails only when a reading matches a rule you switched on; every match
is listed in the job summary and the PR comment with the rule, what the reading
says and the evidence rule id behind it.
{
"version": 1,
"fail_on": ["key_egress_observed", "undeclared_key_read", "unlimited_fund_action", "install_script_added", "new_host"],
"allow_hosts": ["*.base.org", "api.coingecko.com"],
"allow_subjects": ["npm:@acme/wallet"]
}| rule | matches when |
|---|---|
key_egress_observed |
a planted test key was seen leaving during one of Sato Hub's runs |
undeclared_key_read |
code reads key material and the setup does not ask for a key |
unlimited_fund_action |
a fund-moving action has no configurable limit found |
install_script_added |
this version adds an install script |
new_host |
this version adds a request to a host not in allow_hosts (exact host or *.suffix) |
allow_subjects exempts a subject id from every rule. fail_on defaults to
["key_egress_observed"] when omitted.
unknownnever matches a rule. No reading is not a finding.- The install check returns summaries, so the action reads each subject's full
profile from
GET /api/check?target=<id>(four at a time, 30 s timeout). A read that fails is reported as could not evaluate — never a match. - An invalid policy file is reported and ignored; it never fails the build.
- No policy file → the v1.1 behaviour: only
fail_on(input) decides. - New output:
policy_violations(count of matches).
A match names a fact your team asked to be stopped for. It is not a rating of the dependency.
For repositories generated by create-sato-agent. The action reads
sato.create.json and sato.lock.json from the repository root, asks Sato
Hub's POST /api/create/drift what has materially changed since the repo was
generated, and opens exactly one issue per change, ever:
- a newer template version that is green and differs from what you generated;
- a stored Sato Check reading for a package you pin that moved (key access, key
egress, fund actions, or a new host) —
unknownis never reported as a change; - an upstream release, pinned by your lockfile, that broke the template's build.
Each issue carries a hidden marker <!-- sato-drift:<key> --> and the label
sato-drift. Before opening anything, the action reads open and closed
issues. A closed issue is an answer: it is never reopened and never filed again.
No pull requests are opened and no badge is written.
# .github/workflows/sato-drift.yml
on:
schedule: [{ cron: "17 7 * * 1" }]
workflow_dispatch:
permissions:
issues: write
contents: read
jobs:
drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: satohubai/preflight-action@v1
with:
mode: template-driftWithout sato.create.json the step exits 0 with a notice. If the drift endpoint
or the GitHub API cannot be read, the step warns, opens nothing, and exits 0 —
unless you set fail_on_error: true. Outputs: drift_changes, drift_opened.
Only create and lock are sent; nothing else from the repository leaves it.
Everything above is free and needs no key, in public and private repos alike. Sato Check Team is the plan for a team that wants it managed in one place. With a Sato API key the Action also:
- registers the repo in your account's inventory (repo name, visibility and the dependencies it named), so a change in what a dependency does with keys, hosts or funds reaches your alert endpoints and receipt email;
- applies your org policy on top of the repo's
.sato/policy.json. A repo can add rules and narrow the allow-lists; it cannot loosen the org's. Waivers carry a reason and an expiry; - records the run in a history you can read at
satohub.ai/dashboard/check; - uploads the merged-policy SARIF to GitHub code scanning.
permissions:
contents: read
security-events: write # only for the SARIF upload
steps:
- uses: actions/checkout@v5
- uses: satohubai/preflight-action@v1
with:
sato-api-key: ${{ secrets.SATO_API_KEY }}- No key, no change. Without
sato-api-key(orSATO_API_KEY) nothing is registered or stored for your repo and the Action behaves as before (thex-sato-repoheader above is only read as an own/not-own flag). - It fails open. An unreachable API, a rejected key, a plan without the feature or a quota refusal is reported and never fails the build. The only thing that can is a policy rule your own team wrote matching a reading.
- What is stored, and how to remove it. Repo name, visibility, the dependency
names and a record of each run, for keyed callers on a Sato Check Team plan only.
Delete a repo, and its history with it, at
satohub.ai/dashboard/check. - Private repos are checked the same way. The visibility header comes from the workflow's event payload.
- Reads
package.jsonmanifests in this release.
verdict (the most severe seen) · go · caution · no · unknown ·
total · sarif (path to a SARIF 2.1 log) · report (the markdown) ·
team_repo_registered · team_sarif (Sato Check Team mode).
Warn, never block — the setting most repos want:
- uses: satohubai/preflight-action@v1
with:
fail_on: noneA Python project:
- uses: satohubai/preflight-action@v1
with:
manifest: requirements.txtUpload to GitHub code scanning:
- uses: satohubai/preflight-action@v1
id: preflight
with: { fail_on: none }
- uses: github/codeql-action/upload-sarif@v3
if: steps.preflight.outputs.sarif != ''
with:
sarif_file: ${{ steps.preflight.outputs.sarif }}Check the MCP endpoint you are about to ship against:
- uses: satohubai/preflight-action@v1
with:
manifest: "false"
endpoints: "https://mcp.example.com/v1"Any repo can embed a Preflight badge — no account, no setup:
[](https://satohub.ai/preflight)
[](https://satohub.ai/preflight)Badges cover repo and package only. A badge is rendered by whoever opens a
README, at a rate nobody controls — a badge that ran the endpoint lane would
turn every README view into a live probe of somebody's server.
POST https://satohub.ai/api/preflight/batch
{ "manifest": "<raw package.json text>", "manifest_kind": "package.json", "fail_on": "no" }returns { summary, exit_code, results[], findings[], skipped[], caveat },
where findings are SARIF results entries. Single targets:
GET /api/preflight?package=… · ?repo=… · ?endpoint=… · ?agent=… ·
?token=…&chain=…. Full reference: https://satohub.ai/preflight ·
methodology (every rule a verdict can cite): https://satohub.ai/preflight/methodology.
Each call carries an x-sato-repo: <owner>/<repo> header (from GITHUB_REPOSITORY) that Sato Hub uses only to tell its own CI runs of this Action from yours; it stores a bare own/not-own flag, never the repo name.
MIT. Data from https://satohub.ai.