Skip to main content

fremforge CLI

The fremforge CLI is a single-binary wrapper around the public REST API. Same verbs, same authorization model, same scopes. Distributed from EU PoPs only; no npm registry, no Docker Hub, no Sigstore Fulcio dependency.

Support period, security updates, and SBOM

Support period5 years from the release date of each minor version. This is the support period within the meaning of Art. 13(8) of the Cyber Resilience Act (Regulation (EU) 2024/2847), and it attaches to the released version, not to your subscription.
Security fixesFree, always, and never gated behind a plan. Fixes ship forward as a new release at the same URLs, covered by the same signed SHA256SUMS. Running a superseded version? Upgrade to get the fix.
Reporting a vulnerabilitysecurity@frem.sh — coordinated disclosure, details at www.frem.sh/security.
SBOMCycloneDX JSON at cli.frem.sh/cli/SBOM.cdx.json, per-version at cli.frem.sh/cli/v<version>/SBOM.cdx.json.
Contract termsToS §7.5 — licence, warranty, and liability for the binary specifically.

The SBOM is short, and that is the point. The CLI declares zero third-party npm dependencies. It has exactly one dependency, @fremverk/audit-chain, which is ours: the same hash-chain code the API itself runs, shared rather than reimplemented so that audit-verify cannot drift from the thing it verifies. That package has no dependencies of its own and imports only Node builtins, so the entire third-party surface of the shipped binary is still the Bun runtime embedded at compile time. One component is the honest answer, not an oversight.

The SBOM hash is listed in SHA256SUMS, so it is covered by the same Ed25519 signature as the binaries — verify it the same way (see §“Verify the Ed25519 signature” below — it applies to every file listed in the manifest, not just the binaries):

curl -fsSL https://cli.frem.sh/cli/SBOM.cdx.json -o SBOM.cdx.json
curl -fsSL https://trust.frem.sh/SHA256SUMS -o SHA256SUMS
grep SBOM.cdx.json SHA256SUMS | shasum -a 256 -c -

The CLI is optional, with one exception. Every action it performs is available through the REST API, so if your policy is not to run vendor binaries you lose no functionality by skipping it. The exception is audit-verify, which recomputes your audit hash chain on your machine — an API cannot do that for you, because the entire point of recomputing a hash chain is not to trust the party that produced it. The raw rows are served by GET /orgs/{org}/audit/chain, so you can do the arithmetic in your own tooling instead; what you cannot do is have us do it and call the result verification.

Install

One-liner (recommended)

curl -sSfL https://cli.frem.sh/cli/install.sh | sh

The installer auto-detects your OS (Linux or macOS) and architecture (arm64 or x86_64), downloads the matching binary from https://cli.frem.sh/cli/, downloads the canonical SHA256SUMS from https://trust.frem.sh/ (a separate host with a separate writer credential, so a compromise of the binary host alone cannot forge a match), verifies the SHA-256, and installs to ~/.local/bin/fremforge or /usr/local/bin/fremforge.

Override the install location with FREMFORGE_INSTALL_DIR:

curl -sSfL https://cli.frem.sh/cli/install.sh | FREMFORGE_INSTALL_DIR=/opt/fremforge/bin sh

Pin a specific version:

curl -sSfL https://cli.frem.sh/cli/install.sh | FREMFORGE_VERSION=0.1.0 sh

Manual download

If you’d rather not pipe a remote script to sh, the binaries are reachable directly. Pick the matching pair:

OSArchURL
macOSarm64https://cli.frem.sh/cli/fremforge-darwin-arm64
macOSx86_64https://cli.frem.sh/cli/fremforge-darwin-x64
Linuxarm64https://cli.frem.sh/cli/fremforge-linux-arm64
Linuxx86_64https://cli.frem.sh/cli/fremforge-linux-x64
Windowsx86_64https://cli.frem.sh/cli/fremforge-windows-x64.exe

The architecture suffix is x64 (not x86_64) and arm64 — these match Bun’s --target tokens. uname -m reports x86_64 on Intel/AMD, so map it to x64 when building the URL by hand (the one-liner installer does this for you).

Verify against the canonical hash on trust.frem.sh before running the binary:

# 0. Map uname -m's x86_64 to the published x64 token
arch=$(uname -m); [ "$arch" = x86_64 ] && arch=x64

# 1. Download the binary
curl -fsSL https://cli.frem.sh/cli/fremforge-$(uname -s | tr A-Z a-z)-$arch \
  -o fremforge

# 2. Download the canonical SHA256SUMS (separate host, separate credential)
curl -fsSL https://trust.frem.sh/SHA256SUMS -o SHA256SUMS

# 3. Verify
sha256sum --check --ignore-missing SHA256SUMS

# 4. Install
chmod +x fremforge
sudo install -m 0755 fremforge /usr/local/bin/fremforge

Verify the Ed25519 signature (optional, recommended)

Beyond the SHA-256 match, each release is signed as a DSSE-wrapped in-toto Statement under the same trust root as fremforge SLSA build provenance. The verifier downloads the public trust root from www.frem.sh/.well-known/slsa-trust-root.json and checks the Ed25519 signature on the SHA256SUMS bytes.

# Pull the trust root and the signed envelope
curl -fsSL https://www.frem.sh/.well-known/slsa-trust-root.json -o slsa-trust-root.json
curl -fsSL https://trust.frem.sh/SHA256SUMS.intoto.jsonl -o SHA256SUMS.intoto.jsonl

# Verify using slsa-verifier
slsa-verifier verify-artifact fremforge \
  --provenance-path SHA256SUMS.intoto.jsonl \
  --source-uri https://frem.sh/internal-fremforge/monolith \
  --builder-id https://frem.sh/runner-controller/v1 \
  --trusted-root slsa-trust-root.json

The trust root is the same one used for SLSA-L2 build provenance on artifacts your runner produces, same Ed25519 key, same builderId, same verification recipe.

Authentication

The CLI supports three credential modes, resolved in this order: --token=… flag, FREMFORGE_TOKEN env var, then the stored login at ~/.config/fremforge/auth.json.

Recommended — browser-based login (no paste)

fremforge auth login

Opens your browser to frem.sh, you approve the device, the token lands at ~/.config/fremforge/auth.json (mode 600) and every subsequent invocation auto-loads it. Same OAuth2 + PKCE flow your git push already uses, so revoking the token at frem.sh/user/settings/applications revokes git access and CLI access in one step.

fremforge auth status      # show current login + expiry
fremforge auth logout      # revoke + delete the local file

The token works on both the fremforge REST API (/api/v1/orgs/…) AND Forgejo’s native API (/api/v1/repos/…) — Forgejo accepts its own OAuth bearers natively, and the fremforge api validates them via introspection against Forgejo’s /api/v1/user. Same identity, both surfaces.

CI / scripted use — PAT

For non-interactive contexts (CI jobs, scripts, agents), mint a scoped PAT. Easiest is the CLI itself once you’ve logged in interactively once (fremforge auth tokens create --name=… --scopes=… — see Mint a scoped PAT for CI); or mint one at https://frem.sh/user/settings/applications. Then:

export FREMFORGE_TOKEN='<your PAT>'
fremforge whoami

Override the base URL with FREMFORGE_URL= (must be HTTPS); default is https://frem.sh.

Commands

fremforge auth login                              Browser-based OAuth2+PKCE login.
fremforge auth logout                             Revoke + delete the stored token.
fremforge auth status                             Show current login + expiry.
fremforge whoami                                  Print the user the token authenticates as.
fremforge data-export start <slug>                Queue a full data export for the org.
fremforge data-export list <slug>                 List recent export jobs.
fremforge dependency-updates enable <slug>        Enable hosted Renovate for the org.
fremforge dependency-updates disable <slug>       Disable hosted Renovate for the org.
fremforge dependency-updates run-now <slug>       Trigger an out-of-cycle Renovate run (1/h cap).
fremforge audit-log <slug>                        Fetch the org audit-log slice.
                  [--since=DATE] [--actor=…] [--action=…] [--limit=N]
fremforge audit-verify <slug>                     RECOMPUTE the per-tenant audit hash chain
                  [--human] [--full]              locally, then cross-check our own verdict.
                                                  Exit 0=ok, 2=partial, 3=broken,
                                                  4=anchor_mismatch, 5=local ok but we
                                                  disagreed. --full walks from the chain
                                                  epoch instead of the latest anchor.
fremforge sbom <owner>/<repo> <tag>               Download a release SBOM.
                  [--format=spdx|cyclonedx]
fremforge runs <owner>/<repo>                     List recent CI runs (JSON; --human for a table).
                  [--limit=N] [--status=…] [--event=…]
fremforge runs logs <owner>/<repo> <run-id>       Fetch a run's logs (text/plain, all jobs).
fremforge auth tokens create                      Mint a scoped PAT (printed once).
                  --name=N --scopes=a,b [--ttl-days=N]
fremforge auth tokens list                        List your PATs (--human for a table).
fremforge auth tokens revoke <token-id>           Revoke a PAT.
fremforge findings <type> <slug>                  Security findings. type ∈ secrets |
                  [--status=…] [--repo=…]         dependencies | signed-commits | attestations.
fremforge secrets list <slug>                     List org Actions/CI secret names.
fremforge secrets set <slug> <NAME>               Set a secret (value via FREMFORGE_SECRET_VALUE).
fremforge secrets delete <slug> <NAME>            Delete a secret.
fremforge seats list <slug>                       List org members + seat usage.
fremforge seats add <slug> --email=E              Add a member (--username=, --full-name= optional).
fremforge seats remove <slug> <username>          Remove a member.
fremforge version                                 Print the CLI version.

Run fremforge --help for the canonical reference, or pipe to --human on any command that supports it for table-formatted output.

Worked examples

Verify your audit chain yourself (use in CI)

audit-verify does not ask us whether your audit log is intact. It downloads the raw rows, recomputes every hash and every link on your machine, and only then compares its result with ours. The exit code is your result, not our claim — and if the two disagree, that disagreement is itself reported and the command fails.

That distinction is the reason this command exists. Until August 2026 it read a integrity_status field out of an API response and mapped it to an exit code, which means the party being audited computed the answer and the “verification” tool relayed it. Nothing you could run would have caught a compromised or dishonest API. Now something does.

export FREMFORGE_TOKEN='<your PAT, audit:read scope>'

# Human-readable, for terminals
fremforge audit-verify acme --human

# Script-friendly. The exit code reflects the LOCAL walk:
#   0 = ok
#   2 = partial          — some of the chain could not be checked (see below)
#   3 = broken           — a row's content or its link does not match. Investigate.
#   4 = anchor_mismatch  — the WORM-anchored tip is not present in your chain
#   5 = we disagreed     — your walk was clean but our verdict was not
#   6 = bad signature    — the anchor's signature failed against our published key
fremforge audit-verify acme || {
  echo "audit verification failed with exit code $?"
  exit 1
}

# Walk the entire chain from its first row instead of just the tail since the
# last anchor. Slower and much larger; appropriate for a quarterly review.
fremforge audit-verify acme --full --human

Reading the results honestly, since a verification tool that overstates itself is worse than none:

  • partial is not a pass. It means truncation hid part of the chain, so the rows outside the window are unverified. We report ok in that case and the CLI deliberately does not.
  • Erased and redacted rows do not rehash, by design. GDPR erasure and the 90-day payload reaper rewrite row contents after the fact, so those rows no longer match their own hash. They are counted separately (tenant_erased_count, payload_redacted_count) and are not tampering. Their links are still checked.
  • The WORM anchor is signed, and the CLI checks it against a trust root it fetches itself. audit-verify pulls www.frem.sh/.well-known/slsa-trust-root.json directly — never a key from the API — verifies the Ed25519 DSSE signature, and confirms the envelope is bound to this anchor rather than some other one. Exit 6 means the signature was checked and failed; treat that as an incident. NOT CHECKED (exit unchanged) means either the anchor predates signing (2026-08-04) or the trust root was unreachable from your machine — neither is a failure, and the CLI says which.

To do the same thing without the binary, read the rows from GET /orgs/{org}/audit/chain and hash each row’s canonical JSON with keys ordered action, actor, created_at_iso, fields_json, prev_hash, tenant_id. Keep fields_json byte-exact — do not parse and re-serialise it, or your digests will not match.

See audit chain integrity for the full guarantee.

Self-service data export

# Kick off
fremforge data-export start acme | jq .

# Poll
fremforge data-export list acme | jq '.[] | {id, status, created_at}'

See data export for the artifact format, retention, and signed-URL download flow.

Dependency updates (hosted Renovate)

fremforge dependency-updates enable acme
fremforge dependency-updates run-now acme   # 1/h rate-limit applies

See dependency updates for what gets enrolled and how to scope it per-repo.

SBOM download

fremforge sbom acme/api v1.4.0 --format=spdx > sbom.spdx.json
fremforge sbom acme/api v1.4.0 --format=cyclonedx > sbom.cyclonedx.json

Audit log slice

fremforge audit-log acme --since=2026-05-01 --action=push-protection-override | jq .

CI runs + logs

List recent runs, then pull a run’s full logs (every job concatenated) by its global run id:

# List the 20 most recent runs as a table
fremforge runs acme/api --human

# Script-friendly: grab the newest run id, then fetch its logs
run_id=$(fremforge runs acme/api --limit=1 | jq -r '.workflow_runs[0].id')
fremforge runs logs acme/api "$run_id"

# Tail just the failing step
fremforge runs logs acme/api "$run_id" | grep -iE "error|failed|exit code" | tail

<run-id> is the global run id shown by fremforge runs (the .workflow_runs[].id field) — not the per-workflow run number in the web UI URL. The logs endpoint returns text/plain with all jobs in one response; no per-job artifact download needed.

Mint a scoped PAT for CI

After your first fremforge auth login, mint narrow-scoped child tokens for CI without leaving the terminal. The token is printed once — capture it immediately:

fremforge auth tokens create --name=ci-readonly --scopes=findings:read,orgs:read --ttl-days=30 \
  | jq -r .token
fremforge auth tokens list --human
fremforge auth tokens revoke <token-id>

You can only grant scopes your own token already holds, and wildcard (*) cannot be minted here.

Manage CI secrets

fremforge secrets list acme

# Pass the value via env (preferred — not in shell history or `ps`)
FREMFORGE_SECRET_VALUE="$(cat ./deploy-key)" fremforge secrets set acme DEPLOY_KEY

fremforge secrets delete acme OLD_TOKEN

Seats / members

fremforge seats list acme --human                       # cap + usage + roster
fremforge seats add acme --email=jane@acme.io --full-name="Jane Doe"
fremforge seats remove acme jane

Adding a member over the contracted seat cap returns a 409 with a billing link — buy more seats first.

Output format

By default every command emits JSON to stdout, script-friendly and pipe-friendly. Pass --human on the commands that support it (currently audit-verify) for a formatted summary.

Updating the CLI

The one-liner installer is idempotent, running it again pulls the latest binary, re-verifies the checksum, and overwrites the old binary in place:

curl -sSfL https://cli.frem.sh/cli/install.sh | sh

Sovereignty notes

The CLI distribution path is fully EU-sovereign:

  • Binaries: T Cloud Public OBS (eu-de, Magdeburg / Biere, Germany), behind a Bunny pull-zone restricted to EU PoPs only.
  • SHA256SUMS + DSSE envelope: A separate T Cloud Public OBS bucket (trust.frem.sh) with a separate writer credential, splitting the binary host from the integrity host means compromising one cannot forge a match against the other.
  • Trust root: Static JSON file served from www.frem.sh (same Bunny EU-only routing).
  • No npm registry dependency: registry.npmjs.com runs in US AWS; the CLI never touches it on install or on use.
  • No Docker Hub dependency: same reason.
  • No Sigstore Fulcio / Rekor: Sigstore’s transparency log is US-hosted. fremforge runs its own Ed25519 signer + a static trust root rather than depending on Fulcio.

Sub-processors involved on the install path are listed in the public sub-processor register and are the same ones already covering customer Personal Data per the DPA Annex B.