# Fwozen > Org-wide merge and deploy freezes for GitHub. Freeze from the web, from Slack, from the REST > API, from the CLI, or on a schedule — with an audit trail that names an actor in every row. Fwozen is a GitHub App plus a web dashboard, a Slack app, a CLI, and a REST API. It blocks merges using a required check run named `fwozen`, refuses a requested deployment freeze through a `fwozen/deploy-gate` check and a GitHub Action your pipeline calls, reads GitHub's effective branch rules for each watched branch to report what it found, and records every action with a real actor. ## Getting started as an agent You can take an organisation from nothing to an enforced freeze without a human driving a browser for anything except consent. Four moments need a person, and they are marked HUMAN below. Nothing in this API charges a card, and nothing here should be run on your own initiative — see "What an agent must not decide" at the end of this section. Sign in (HUMAN #1 — approves the code in a browser). Fwozen runs its OWN authorization server at https://fwozen.com/api/auth; sign-in is standard device authorization, RFC 8628: - POST https://fwozen.com/api/auth/device/code with a JSON body `{"client_id": "fwozen-agent"}` and `Content-Type: application/json`. THIS endpoint also accepts the `application/x-www-form-urlencoded` body RFC 8628 illustrates; the TOKEN endpoint below does NOT, so send JSON to both and the whole flow works. The client is PUBLIC — a public client has no secret, so never look for one and never send `client_secret`. - Show the human `verification_uri_complete` and `user_code`. The approval page is https://fwozen.com/device, and getting to an approval takes THREE acts, none of them yours: the link only PREFILLS the code in a form — opening it approves nothing and claims nothing; the signed-in human SUBMITS that form, which CLAIMS the code and reveals the client and the scopes; and approve is then a separate, explicit press. A code no session has claimed cannot be approved, so there is nothing here you can complete on their behalf — do not try. - Poll POST https://fwozen.com/api/auth/device/token with a JSON body carrying `grant_type=urn:ietf:params:oauth:grant-type:device_code`, `device_code`, `client_id` — JSON ONLY on this one: a form-encoded body here is answered with HTTP 415, which is why a client that follows RFC 8628 literally starts a login successfully and can never finish it. `authorization_pending` means keep waiting; `slow_down` means add 5s to your interval; `access_denied` and `expired_token` are terminal. The device code expires in ~30 minutes. - What comes back is a SESSION TOKEN, and it is not the credential the API takes. Store it at 0600 and send it to https://fwozen.com and nowhere else. Exchange it for an API token: GET https://fwozen.com/api/auth/token with `Authorization: Bearer ` answers `{ token }` — a short-lived RS256 JWT, which is what https://api.fwozen.com takes as `Authorization: Bearer `. Cache the JWT until it expires; do not mint one per request. - There is no refresh token and no rotation to get wrong. The session is what expires, a device-minted session is fixed-length and does NOT extend by being used, and POST https://fwozen.com/api/auth/sign-out with the session token revokes it immediately — which is what you do the moment a credential might have leaked. - Org switch: POST https://fwozen.com/api/auth/org/set-active with the session token and `{"organizationId": ""}`, then fetch a fresh `/api/auth/token`. That is the ONLY way to change the `org_id` claim, and the id is `organizations.id` — the same `org_…` id GET /v1/me/organizations returns. Then, against https://api.fwozen.com: 1. POST /v1/onboarding/bootstrap — creates the organisation for the signed-in user. Idempotent. 2. GET /v1/github/install-url — returns the GitHub App install URL carrying a signed `state` nonce. Mint it here; never hand-build an install URL. 3. HUMAN #2 installs the App at that URL. A GitHub organisation owner has to do it, because Fwozen requests one organisation permission (members:read). If the person is not an owner, GitHub files an approval request and the install sits pending — GitHub sends nothing in between, so an agent that assumes failure after 30 seconds is wrong. 4. GET /v1/installations/status?state= — poll it. Answers `pending`, `installed_unclaimed` or `installed_claimed`. - `installed_claimed` is the finish line and the only status carrying ids. Go to step 5. - `pending` means nothing has landed yet, and see HUMAN #2: it can last hours. - `installed_unclaimed` means an installation landed on an organisation nobody has proved they administer — the Marketplace-first order, where the App was installed before anybody bootstrapped. It names nothing and returns no ids, so POST /v1/installations/claim cannot resolve it: that route needs GitHub's numeric `installation_id`, which only the browser redirect straight after Install ever carries, and it needs an organisation you are already in. The repair is the GitHub organisation claim, and it is a browser trip end to end (HUMAN): the person opens https://fwozen.com/api/github/claim while signed in to the dashboard, GitHub asks which organisations they administer, and their browser redeems the single-use ticket that comes back at POST /v1/onboarding/claim-organization. You cannot make either leg yourself — the first is a top-level navigation on the web origin (the API takes credentials on the `Authorization` header only), and the ticket never leaves that browser. When they say it is done, switch your session to that organisation — GET /v1/me/organizations names it and gives you its `org_…` id, then POST https://fwozen.com/api/auth/org/set-active and fetch a fresh `/api/auth/token` — and poll this route once more. It now answers `installed_claimed`. Doing step 1 FIRST is what avoids this state: an organisation you are a member of is claimed by construction, so the poll goes straight from `pending` to `installed_claimed`. 5. POST /v1/repos/enable with an explicit repository list, then POST /v1/repos/{repoId}/branches for each branch to watch. Fwozen NEVER enables a repository on its own — private repos are the billable unit and enabling one silently is a charge nobody agreed to. Ask, then enable what the human named. 6. GET /v1/enforcement/branches/{branchId}/snippets — gh CLI, Terraform and ruleset JSON for the one native GitHub rule. HUMAN #3 makes the check required. On an installation that has granted repository administration that is a button in the dashboard and Fwozen writes the ruleset itself; otherwise they save the rule in GitHub's own settings. Setting enforcement up is never yours to do either way: enforcement setup is session-only — POST /v1/enforcement/setup refuses an `fwz_` key with 403 `credential_type_not_allowed`, and no scope in the list below grants it. POST /v1/enforcement/verify does take a key, and reports what GitHub actually says. 7. POST /v1/billing/trial starts the 14-day Business trial: no card, and it does NOT auto-convert — at expiry the organisation downgrades to Free. Nothing is charged. It is once per organisation AND once per person: a second organisation does not earn a second trial, and asking for one answers 409 `trial_already_used`. 8. POST /v1/billing/checkout returns `{ url }` for a Stripe Checkout page. Hand that URL to HUMAN #4 and stop. No Fwozen route, tool, or CLI command ever completes a payment. Credentials for unattended automation: - `fwz_` API keys are the automation credential: `Authorization: Bearer fwz_…`, flat scopes (`freeze:read`, `freeze:write`, `repo:read`, `repo:write`, `schedule:read`, …) that cannot be widened after creation. - `billing:read` and `billing:write` exist but are NOT granted by default — the human minting the key has to tick them. Neither can move money: with `billing:write` a key can start the no-card trial and ask for a Checkout or Portal URL, and that is where its involvement ends. - A key may declare itself an AGENT key at creation. An agent key must expire within 90 days, and everything it does is labelled as an agent's action in the audit log — distinct from the human who authorised it. - Keys are minted by POST /v1/api-keys, and that route takes a SESSION only. A key can never mint another key. The device-flow session above is how an agent gets its first key. A human makes one at https://fwozen.com/app/settings/api-keys as an owner or an admin. Create key, tick the scopes, and the secret is shown once. - The secret is shown once. Never print, log, or echo it. Rate limits an unattended caller will meet: - Freezes and repository changes carry a per-CREDENTIAL write limit on top of the organisation's own per-tier ceiling, so one retry-happy agent cannot spend the whole organisation's budget. Lifting a freeze is exempt from that per-CREDENTIAL bucket — the way out is never the thing one runaway client can spend. It is NOT exempt from the organisation's ceiling: a lift is an ordinary write there and can answer 429, so honour `Retry-After` on a lift like any other write. - Creating organisations is limited too, both per person per day and in how many may be held with no GitHub App installed. - No numbers are published here, and that is deliberate: they are operational settings tuned per deployment, so a figure quoted in this document would be a promise we did not make. Read `RateLimit-Limit` / `RateLimit-Remaining` / `RateLimit-Reset` off every CREDENTIALLED /v1 response, and GET /v1/entitlements for the per-tier ceilings. Size retry budgets from those, never from prose. The two routes that take no credential — GET /v1/plans and GET /v1/github/oauth/callback — sit outside the organisation bucket and carry no rate headers at all, so do not read a budget off them. - Every REST refusal is a 429 with `Retry-After`. On MCP, a refused tool call comes back as an `isError` result whose text carries the same retry seconds — there are no rate headers on a tool result. Honour the wait either way; retrying faster will not help. If you speak MCP, the same journey is a tool call: - Streamable-HTTP MCP endpoint: https://api.fwozen.com/mcp. One POST, no session store. - Send the SAME credential you would send the REST API: `Authorization: Bearer ` (the short-lived token from `/api/auth/token` above) or `Authorization: Bearer fwz_…`. There is no MCP-specific credential to go looking for. A request without one answers 401 with a `WWW-Authenticate` header naming https://api.fwozen.com/.well-known/oauth-protected-resource (RFC 9728), which lists the authorization server. - The tools are thin adapters over the routes above and carry the same rules: reads and writes are never mixed in one tool, `fwozen_enable_repos` takes an explicit repository list, `fwozen_checkout_link` returns a URL and nothing else, and `fwozen_end_freeze` is marked destructive and refuses without `confirm: true`. Machine-readable surfaces: - OpenAPI 3.1 spec: https://api.fwozen.com/v1/openapi.json - This file, plus every documentation page and the public route table: https://fwozen.com/llms-full.txt (also at https://api.fwozen.com/llms-full.txt) - App + auth discovery document: https://api.fwozen.com/.well-known/fwozen-app (also at https://fwozen.com/.well-known/fwozen-app) - Agent registration guide (auth.md convention): https://api.fwozen.com/auth.md (also at https://fwozen.com/auth.md) - Agent policy — what agents may do here: https://fwozen.com/agents - Agent skill index: https://fwozen.com/.well-known/skills/index.json → https://fwozen.com/skills/fwozen/SKILL.md What an agent must not decide: - Never start or lift a freeze on your own judgement. A freeze blocks every engineer in the organisation; a thaw unblocks a merge someone deliberately stopped. Both need an explicit human instruction, and `reason` is required. - Never enable repositories that a human did not name. - Never treat "the API was unreachable" as "not frozen". The gate fails closed on purpose. - Never report a branch as protected because enrollment succeeded. Read the verification fields. ## Facts - Pricing: Free $0 (5 private repos) / Team $29/mo / Business $99/mo. There is no Enterprise tier — that is the whole ladder, and the top of it is self-serve. Annual is 2 months free. No per-user charge on any plan, including Free. Unlimited private repos on every paid plan. - Scheduled and recurring freezes, the Slack app and the REST API are included in the FREE tier. Any workspace can install the Slack app; it is not listed in the Slack Marketplace. - The `fwozen/gate-action` CI step and `GET /v1/freeze-state` are on EVERY plan, Free included, and a Free freeze can carry `blocksDeploys`, so a Free pipeline already fails closed. What starts at Team ($29/mo) is `fwozen/deploy-gate`, the second required check you switch on per branch, alongside per-PR exceptions and unlimited private repos. - Permissions Fwozen ASKS FOR, on the REPOSITORY and ORGANISATION axis — eight: checks:write, statuses:write, pull_requests:read, contents:read, metadata:read, merge_queues:read, org members:read, administration:write. Fwozen never requests contents:write, and organisation administration is requested in neither direction — so an organisation ruleset and your organisation's settings stay out of reach. Installing grants every permission Fwozen asks for, repository administration included. An installation made before Fwozen asked for it keeps its old set until an organisation owner approves, and some never will. Every live installation grants at least seven of the eight. Repository administration:write is granted by every new installation, and by an older one when its owner accepts it; it exists to write the one ruleset that makes the fwozen check required, it can edit no rule Fwozen did not create, and no endpoint deletes a ruleset at all. The full inventory, including that row and where its rollout stands, is at https://fwozen.com/security. Where Fwozen is not writing that rule — an installation without the permission, a repository marked as managed as code, and organisation scope always — setup is one native GitHub rule that a person with that authority saves in GitHub's own settings; Fwozen shows the exact values and verifies afterwards. - On the ACCOUNT axis there is exactly ONE: email_addresses:read. It is a different axis and it is disclosed here so the count above is not read as the whole consent screen. It is granted by the individual who authorises at sign-in, not by the organisation that installs the App, it confers nothing over any repository, and it exists only so sign-in can identify a person by their verified email — GitHub returns no email at all for an account whose address is private. The sign-in request itself carries the `read:user` and `user:email` scopes, sent by better-auth against the GitHub App's own client id; the App's account permission set is the one row above. - Verification: for GitHub rulesets, Fwozen verifies whether its check is active, required, and pinned to the Fwozen App, and separately reports what GitHub exposed about bypass actors. Classic branch protection is guidance-only — the effective-rules endpoint reports rulesets rather than classic protection, and no classic read has been demonstrated against a live classic rule. Bypass assurance is usually `unknown`, because GitHub rarely discloses a ruleset's bypass list to the app reading it, and holding repository administration has not been measured to change that; missing evidence is never reported as "nobody can bypass this". - Merge-group freezing is implemented and unit-tested, but has not been demonstrated against a live GitHub merge queue. We claim the mechanism, not the proof. - No SAML single sign-on, no SCIM directory sync, no incident triggers, and no policy-as-code. A freeze starts because a person or a schedule started it. - GitHub Cloud only at launch. No GitLab, Bitbucket, or GitHub Enterprise Server. - There is a CLI and a GitHub Action. Install the CLI with `curl -fsSL https://fwozen.com/install | sh`: one file, Node 24 or newer required, no package manager, and it verifies a published sha256 before it writes anything — `https://fwozen.com/cli/release.json` restates that digest. It is still NOT published to npm, so `npx fwozen` and `npm i -g fwozen` do not work today; that installer and the source tree are the two ways in. The GitHub Action is what a pipeline calls for deploy gating, and CI should call the Action rather than pipe a script into a shell. - If Fwozen is unreachable, a check already written as failure keeps blocking; repos with no active freeze keep their passing check and merge normally. The reconciler converges state after recovery. - You can cancel in the app. No email required. ## Docs Every documentation slug is flat under `/docs/` — the group is navigation, not a path segment. - Install Fwozen: https://fwozen.com/docs/install - Your first freeze: https://fwozen.com/docs/first-freeze - When it does not block: https://fwozen.com/docs/troubleshooting - Scheduled and recurring freezes: https://fwozen.com/docs/schedules - Per-PR exceptions: https://fwozen.com/docs/exceptions - Gating deploys: https://fwozen.com/docs/deploy-gating - REST API: https://fwozen.com/docs/api - Error codes: https://fwozen.com/docs/api/errors - Slack commands: https://fwozen.com/docs/slack - The CLI: https://fwozen.com/docs/cli - Installing Fwozen across an enterprise: https://fwozen.com/docs/enterprise-bulk-install - Migrating from MergeFreeze: https://fwozen.com/docs/migrating-from-another-tool - Leaving Fwozen: https://fwozen.com/docs/uninstall - OpenAPI spec: https://api.fwozen.com/v1/openapi.json ## Comparisons and guides - how enforcement works: https://fwozen.com/how-it-works - vs GitHub rulesets: https://fwozen.com/vs/github-rulesets - what Fwozen is, and isn't: https://fwozen.com/gaps - Black Friday code freeze checklist: https://fwozen.com/guides/black-friday-code-freeze-checklist - scheduling GitHub branch protection: https://fwozen.com/guides/github-branch-protection-schedule ## Contact - security@fwozen.com · hello@fwozen.com · https://status.fwozen.com