> ## Documentation Index
> Fetch the complete documentation index at: https://docs.befailproof.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Publish a policy pack

> Ship your own policies as a GitHub release that anyone can install.

A pack is three files attached to a GitHub release. `failproofai publish` writes all three from the policy files in front of it, creates the release, and uploads them.

## 1. Write the policies

Start from something that already works rather than a template with blanks:

```bash theme={null}
failproofai publish --init
```

That asks what the pack is called, writes `<name>.mjs`, and stops — no network, no git, nothing published. The file it writes is one policy that already blocks `git push --force`. It refuses to overwrite a file that exists.

Policies use the same API as any custom policy. Two extra fields matter for a pack:

```js theme={null}
import { customPolicies, deny, allow } from "failproofai";

customPolicies.add({
  name: "block-refunds",
  description: "Refunds above the approved limit need a human",
  category: "Billing",        // groups it, and is what --category selects on
  defaultEnabled: true,       // switched on by a plain `policies add`
  match: { events: ["PreToolUse"], tools: ["Bash"] },
  fn: async (ctx) =>
    String(ctx.toolInput?.command ?? "").includes("refund")
      ? deny("Refunds need a human. Ask before running this.")
      : allow(),
});
```

`defaultEnabled` defaults to **false** when you omit it. A plain `failproofai policies add` switches on only what you marked — installing a stranger's every policy unattended is not a decision the installer should make for its user.

A policy may also declare `authority: "reviewable"` with a `reviewedBy` list, which lets the Jev semantic evaluator clear its verdict on machines that configure Jev. `failproofai publish` copies both into the manifest, and a machine reads them from there; it refuses to build if a declaration would not be honored, such as a misspelled check name or, in a pack that declares Jev checks, a check it does not declare. Leave them out and the policy is hard. See [Policy authority](/policies/authority).

### Jev checks in a pack

A pack can also carry [Jev checks](/reference/policy-sdk#jev-checks) — `semanticPolicies.add()` — beside its policies, or on their own. A pack is the only way a Jev check reaches a machine: in a local policy file it is never asked. `publish` validates each one with the loader's rules and writes them to the manifest's `semantic` array.

* **Limits.** At most 24 checks per pack. Together, their questions must fit what one Jev request has room for, less what the 16 `FailproofAI/jev-policies` checks take first where both are installed (about 9,100 characters are left) unless the repository is FailproofAI's; `publish` refuses a pack over that budget and prints the numbers. Other packs' checks share the same room, so a check that does not fit beside them is not asked there: `policies add` names it.
* **They are the only checks Jev asks.** Failproof AI ships no Jev checks, so a machine asks exactly what its installed packs declare — yours, beside [`FailproofAI/jev-policies`](/policies/authority#semantic-policy-names) where that is installed. Checks from several packs add up; when their questions overflow what one Jev request can carry, FailproofAI's checks are kept first and the rest are dropped with a warning. A name two packs declare differently is honoured for neither — every policy naming it stays hard — while identical declarations of one name are fine. The 16 `FailproofAI/jev-policies` names are reserved: declared by a pack not installed from a FailproofAI repository, that pack's version is never asked, so `publish` refuses one there; pick names of your own.
* **`reviewedBy` names the pack's own checks.** When the pack declares any, `publish` judges every `reviewedBy` against those names only, so a `FailproofAI/jev-policies` name the pack does not declare itself is refused. A pack with no checks of its own is judged against those sixteen names.
* **Set `--min-cli-version`.** A CLI too old for Jev checks ignores the `semantic` array and installs the rest, so pass `--min-cli-version <version>` for a pack that carries checks. It is written to the manifest as `minCliVersion`: an older CLI refuses to install the pack, and refuses to load it if it is already installed — which, for an `enforce` pack with policies, denies what those policies cover (see [When a pack will not load](/policies/packs#when-a-pack-will-not-load)). The value must be plain semver or `publish` refuses it; a CLI that cannot compare a stored value warns and ignores it. For a pack with checks it must be at least `1.0.8-beta.0`, the first release that runs a pack's checks as published (1.0.7 ignores them, 1.0.7-beta.x replaces the built-in checks with them): `publish` refuses a lower value, and writes `1.0.8-beta.0` when you pass none.

A pack of Jev checks alone (no `customPolicies.add`) is refused by a CLI too old for Jev checks ("pack manifest declares no policies") and ignored if already installed. If a machine refuses such a pack when loading it (a `minCliVersion` it does not meet, a missing or altered artifact), it reports why and denies nothing, because the pack blocks nothing without Jev. Older builds do not all agree: 1.0.7 loads one as an empty pack but denies every tool call if its artifact is missing or altered, and a Jev-capable prerelease before 1.0.8-beta.0 (such as 1.0.7-beta.2) denies every tool call whenever it refuses one, including for a `minCliVersion` above it. So before rolling a machine back, remove the pack (`failproofai policies remove <id>`); `publish` prints this reminder for a pack of Jev checks alone.

Write as many files as you like; one per category reads well. Every file in the directory that registers policies is bundled into the single artifact a pack has to be.

<Warning>
  Bundling needs **bun**. Without it, keep to one self-contained file. Either way the published entry must not import local files at install time: only the entry is digest-pinned, so a pack that reached for siblings could not honestly claim the digest covers what runs — and `publish` refuses one rather than shipping a promise it cannot keep.
</Warning>

## 2. Try it here first

Before anyone else can see it, enforce the file on this machine:

```bash theme={null}
failproofai policies -i -c ./<name>.mjs
```

Any path, any filename. Ask your agent to do the thing you blocked and watch it get refused. Nothing is published and nobody else is affected. [Test a policy](/policies/test) covers the rest: the legitimate case it must allow, and the inputs that break it.

## 3. Publish it

```bash theme={null}
failproofai publish
```

It works out where to publish, what to bundle and what version to call it, and only asks when nothing in the repository tells it. In order, stopping before it creates a release if anything is wrong:

1. Finds the policy files here by **content** — those that import `failproofai` and call `customPolicies.add` or `semanticPolicies.add` — rather than by filename, so it finds `guards.mjs` and ignores an unrelated `policies.mjs`. It does not descend into subdirectories, so a test fixture is never swept up by accident.
2. Reads the repo from `git remote get-url origin`, in the **file's** directory rather than yours, and decides the version.
3. Finds your credential: `GITHUB_TOKEN`, `GH_TOKEN`, or `gh auth login`. It needs release-write and nothing else, and is never printed.
4. Creates the repository if it does not exist. This happens before the build, so a pack refused in the next step can leave a new repository behind with no release in it.
5. Builds the three assets, validating them with the **loader's own rules** — the same code that decides what may install on a stranger's machine — so a pack that could never install fails here, where you can still fix it.
6. Creates or reuses the release and uploads, replacing assets of the same name.

| File | What it is |
| - | - |
| `failproofai-pack.json` | The manifest: id, version, effect, one entry per policy, and — when there are any — the Jev checks (`semantic`) and `minCliVersion` |
| `failproofai-pack.mjs` | Your bundled entry |
| `SHA256SUMS` | `<sha256>  <filename>` for the other two |

The asset names are fixed — they are what a consumer's CLI constructs its URLs from, with no API call and no discovery.

Refused at build time: an id that is not `publisher/name`, a policy name containing `/`, a policy declaring `alwaysOn`, a missing `description`, `category` or `match`, an entry that registers nothing, an entry that imports local files, and a Jev check named after a built-in check unless the repository is FailproofAI's.

Override anything it decided:

```bash theme={null}
failproofai publish \
  --repo acme/support-agent \
  --version 1.0.0 \
  --effect observe \
  --dry-run
```

`--id` sets the pack id when it should differ from the repo, `--tag` sets the release's tag, `--notes` replaces the generated release notes — which is where `policies show --releases` reads each release's counts and commit from — `--out` chooses where the assets are written (default `dist-pack`), `--min-cli-version` sets the oldest CLI that may install the pack ([above](#jev-checks-in-a-pack)), and `--dry-run` builds them without publishing and needs no credential.

Anyone can now install it with `failproofai policies add acme/support-agent`. See [policy packs](/policies/packs) for pinning a version and taking only part of one.

### List it on the policy hub

Add the `failproofai-policies` topic to the repository on GitHub. There is no submission form and no approval queue: the [policy hub](https://befailproof.ai/policy-hub/)'s crawler picks the repository up on its next pass. The topic only puts it up for consideration — what lists it is a release whose manifest verifies against its own `SHA256SUMS` and parses under the same rules the CLI uses, which is exactly what `failproofai publish` produces.

## How the version is decided

The version is the **commit you are publishing from** — its short sha, twelve characters: `a1b2c3d4e5f6`. There is nothing to pick and nothing to increment, and the version names exactly where the bytes came from, so publishing the same source twice gives the same version.

It is read from the tree in front of you, never from the repository's releases, so a fresh clone and an air-gapped machine compute the same answer without asking GitHub what happened before.

Because the version names a commit, that commit has to exist. At a terminal, `publish` makes it for you: it initialises a repository when there is none, and commits changed policy files before it builds. It **refuses** instead — naming `--version` as the way out — when it runs without a terminal (a commit made on a CI runner would exist nowhere else), when files other than the policies are uncommitted, or in a checkout that has no commits yet. A tag on `HEAD` wins over the sha — someone who tagged `v1.2.0` has said what this release is.

A sha carries no ordering of its own, so use `failproofai policies show <owner>/<repo> --releases` to see which release came first — newest at the top.

## Shipping a new version

Commit the change and run `failproofai publish` again — the new commit is the new version. Consumers run the same `failproofai policies add`. Without a terminal, or with a selection flag, they keep the subset they had chosen and a policy they turned off stays off; at a terminal with no flag, the picker opens pre-ticked with your defaults and their answer replaces their selection.

Changing a policy's **name** is a breaking change: a machine that had turned it off is turning off a name that no longer exists, and the new name arrives at whatever `defaultEnabled` says.

## What your users are trusting

`SHA256SUMS` lives in the same release as the artifact, so it proves the bytes are the ones you published — not who you are. Whoever can write to the repository can write both files. Your users' protection is that the digest is pinned when they install, so what you shipped cannot change under them afterwards.

Publish from a repository whose write access you control, and treat a pack release like publishing a package.

The repository must also be **public**. Installs are anonymous HTTPS with no credential to offer, so an existing private repo is refused before anything is built or uploaded, and one `publish` creates is public for the same reason. `--allow-private` overrides that for somebody handing the three assets over another way, and says plainly that no `policies add` can reach them. Only the release matters: installs read `releases/download/<tag>/<asset>` and never touch your git tree.

## Observe before you enforce

A manifest may declare `"effect": "observe"` — `failproofai publish --effect observe` is what sets it. Those policies run and their verdicts are **recorded and discarded** — nothing is blocked. An observe pack's Jev checks are not asked at all, and neither are those of a pack installed with `--cli` for other agents. It is the way to measure a new rule against real traffic before it can interrupt anyone's work.

```json theme={null}
{ "id": "acme/support-agent", "version": "a1b2c3d4e5f6", "effect": "observe", "policies": [ ... ] }
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.