Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3635e82
test(docs): scaffold bats doc-validation harness
Tombar Jun 2, 2026
baa5ed7
test(docs): markdown fenced-block extractor with heading anchors
Tombar Jun 2, 2026
d8445eb
test(docs): harden extractor (literal fence match, count reset, statu…
Tombar Jun 2, 2026
24062e2
test(docs): sandbox HOME, tool guards, mode-file helpers
Tombar Jun 2, 2026
5b8678c
test(docs): harden sandbox teardown guard and mode-file CR/LF trim
Tombar Jun 2, 2026
ddf108a
test(docs): cross-cutting chain/canonical/alias claims
Tombar Jun 2, 2026
e2faad3
test(docs): reset state per ro-alias so each alias is actively verified
Tombar Jun 2, 2026
e74dc23
test(docs): claude-statusline.sh contract + graceful degrade
Tombar Jun 2, 2026
f60e512
test(docs): assert jq-degrade branch skips enrichment (no cwd)
Tombar Jun 2, 2026
685daab
test(docs): live headless tmux toggle/status/keybinding validation
Tombar Jun 2, 2026
e43abf3
test(docs): wezterm lua file-contract + luacheck + sudo-timeout
Tombar Jun 2, 2026
f302f06
test(docs): make wezterm luacheck test non-vacuous (skip if runtime b…
Tombar Jun 2, 2026
c2a9117
test(docs): widen wezterm sudo-timeout revert margin for CI
Tombar Jun 2, 2026
f875a3c
test(docs): iterm OSC roundtrip + python read_mode/compile/import
Tombar Jun 2, 2026
076f099
docs(iterm): drop unused async_get_app call (pyflakes: app assigned b…
Tombar Jun 2, 2026
419f7e0
ci: separate doc-validation workflow (headless bats suite)
Tombar Jun 2, 2026
a6dfe03
ci(docs): make tmux keybind + wezterm luacheck checks portable across…
Tombar Jun 2, 2026
d3c1b26
test(docs): live WezTerm config-load check, iTerm manual steps, findi…
Tombar Jun 2, 2026
0d2cdb9
test(docs): narrated demo + manual GUI test plan for live toggle vali…
Tombar Jun 3, 2026
683c63d
chore: gitignore .claude/ (harness worktrees + session state)
Tombar Jun 7, 2026
5a44d34
mode: canonicalize resolved value (migrate legacy, fail-safe to enabled)
Tombar Jun 7, 2026
671aab5
hook: default guard mode is 'enabled' (protected) when no source reso…
Tombar Jun 7, 2026
a88e2a0
facts: emit failsafe_enabled bool + retain legacy mode string
Tombar Jun 7, 2026
41cc3b6
validate: failsafe_enabled is a known fact field
Tombar Jun 7, 2026
3dacd03
policy: boolean fail-safe gate (not failsafe_enabled==false) + failsa…
Tombar Jun 7, 2026
15536fe
cli+mcp: enabled/disabled vocabulary, rich mode-set aliases, serverIn…
Tombar Jun 7, 2026
4cd35a4
corpus: add failsafe_enabled to fixtures, rename lairguard->failsafe,…
Tombar Jun 7, 2026
cb6f705
docs: enabled/disabled vocabulary, failsafe.* namespace, failsafe_ena…
Tombar Jun 7, 2026
222a5ef
test(docs): harness asserts enabled/disabled + failsafe_enabled boole…
Tombar Jun 7, 2026
4c001ae
cleanup: refresh stale mode-vocabulary comments (enabled/disabled, bo…
Tombar Jun 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .failsafe.rego
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# .failsafe.rego — repo policy for failsafe's own repo.
# Demonstrates: a `block` rule and an `allow_override` rule.
package guard.repo
package failsafe.repo

import future.keywords.if
import future.keywords.in
Expand Down
27 changes: 27 additions & 0 deletions .github/workflows/doc-validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: doc-validation
on:
push:
pull_request:

jobs:
validate-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install harness dependencies
run: |
sudo apt-get update
sudo apt-get install -y bats tmux lua5.4 luarocks jq python3-pip
sudo luarocks install luacheck
# System-wide so imports/binaries are HOME-independent (bats sandboxes HOME).
sudo pip install --break-system-packages iterm2 pyflakes
- name: Build failsafe onto PATH
run: |
mkdir -p "$RUNNER_TEMP/bin"
go build -o "$RUNNER_TEMP/bin/failsafe" ./cmd/failsafe
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
- name: Run headless doc validation
run: bats --print-output-on-failure test/docs/*.bats
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ docs/superpowers/

# mkdocs build output
/site/

# harness worktrees + session state
.claude/
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

---

## [Unreleased]

### Changed

- **Canonical mode vocabulary**: the two mode values are now `enabled` (failsafe active,
bundled blocks fire) and `disabled` (failsafe bypassed, user/repo blocks still apply).
The old strings `read` and `read & write` are retained as **legacy aliases** in the
`mode set` input parser and the `input.mode` fact field for back-compatibility with
existing scripts and Rego rules.

- **Primary Rego fact is now `input.failsafe_enabled` (bool)**: `true` when the mode is
`enabled`, `false` when `disabled`. Bundled policies gate on
`not input.failsafe_enabled == false` (fail-safe form: unknown == not-false == block).
The legacy field `input.mode` (string, `"read"` / `"read & write"`) is retained and will
continue to work in existing rules — no migration required.

- **Rego namespace `failsafe.*`**: bundled policies now use `failsafe.bundled.<tool>`;
user policies use `failsafe.user`; repo policies use `failsafe.repo`. The legacy
`guard.*` namespace (`guard.bundled.*`, `guard.user`, `guard.repo`) is still honored
(dual-namespace) — existing `.failsafe.rego` files with `package guard.repo` do not need
to change.

- **Expanded `mode set` aliases**: `failsafe mode set` now accepts a richer set of
case-insensitive aliases.

| Input | Canonical value written |
|-------|------------------------|
| `enabled`, `enable`, `on`, `closed`, `close`, `lock`, `ro`, `r`, `read`, `safe` | `enabled` |
| `disabled`, `disable`, `off`, `open`, `unlock`, `rw`, `w`, `write`, `sudo` | `disabled` |

- **Default mode is `enabled`**: unchanged in behavior, but now reflected by the canonical
string `enabled` rather than `read`.

- **Block reason prose updated**: bundled policies now emit
`"<tool> <verb> blocked while failsafe is enabled"` instead of
`"<tool> <verb> blocked in read mode"`.

- **Status line / badge vocabulary**: `failsafe mode get` outputs `enabled` or `disabled`;
the bundled `claude-statusline.sh` helper renders `🔒 enabled` / `🔓 disabled`; tab
badges in terminal snippets use `on` / `off`.
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Doc validation harness.
.PHONY: validate-docs validate-docs-live
validate-docs: ## headless doc validation (CI runs this)
bats test/docs/*.bats
validate-docs-live: ## GUI-launch checks (local only; needs WezTerm/iTerm)
bats test/docs/live-gui/*.bats
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,14 @@ you keep another writable.

| Mode | | Behavior |
| --- | --- | --- |
| `read` | 🔒 | **Default.** Bundled policies block mutating verbs across `kubectl`, `helm`, `terraform`, `aws`. |
| `read & write` | 🔓 | Bundled blocks bypassed. User and repo policies still apply. |
| `enabled` | 🔒 | **Default.** Bundled policies block mutating verbs across `kubectl`, `helm`, `terraform`, `aws`. |
| `disabled` | 🔓 | Bundled blocks bypassed. User and repo policies still apply. |

```bash
failsafe toggle # flip the current pane
failsafe mode get # show the effective mode
failsafe mode set rw # read & write (aliases: rw / w)
failsafe mode set ro # read-only (aliases: ro / r)
failsafe toggle # flip the current pane
failsafe mode get # show the effective mode
failsafe mode set disabled # disable failsafe (aliases: off / rw / w / sudo)
failsafe mode set enabled # re-enable failsafe (aliases: on / ro / r / safe)
```

**One-keystroke toggles, badges & status** — bind it in your terminal so you never type the command:
Expand All @@ -126,7 +126,7 @@ policy-as-code. They cascade across three layers: **bundled defaults** → **use
(`~/.config/failsafe/`) → **repo** (`.failsafe.rego`).

```rego
package guard.user
package failsafe.user

import future.keywords.if
import future.keywords.in
Expand Down
6 changes: 3 additions & 3 deletions docs/claude-statusline.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ SPDX-License-Identifier: CC-BY-4.0
# Claude Code status line: always-on guard mode

Claude Code can render a custom status line at the bottom of the session. Wire failsafe
into it and you always see whether the agent is in **🔒 read** (safe) or **🔓 write**
into it and you always see whether the agent is in **🔒 enabled** (safe, failsafe active) or **🔓 disabled**
(it can mutate). No guessing, and no surprise `kubectl apply`.

The snippet: [`examples/claude-statusline.sh`](https://github.com/UndermountainCC/failsafe/blob/main/examples/claude-statusline.sh).
Expand All @@ -19,8 +19,8 @@ current pane's mode (`failsafe mode get`, resolved via the same mode-source chai
uses) and prints, e.g.:

```
failsafe 🔒 read · ~/code/infra · Opus
failsafe 🔓 write · ~/code/infra · Opus
failsafe 🔒 enabled · ~/code/infra · Opus
failsafe 🔓 disabled · ~/code/infra · Opus
```

Because it reads the same per-pane mode the guard enforces, the line flips the instant you
Expand Down
8 changes: 4 additions & 4 deletions docs/explanation/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,13 @@ block contains {"reason": "npm install without --ignore-scripts runs postinstall
input.tool == "npm"
input.verb == "install"
not input.flags["ignore-scripts"]
input.mode == "read"
not input.failsafe_enabled == false
}

# Block npx unconditionally in read mode — it fetches and runs remote code.
block contains {"reason": "npx fetches and executes remote code; flip to write mode if you intend this"} if {
# Block npx unconditionally while failsafe is enabled — it fetches and runs remote code.
block contains {"reason": "npx fetches and executes remote code; disable failsafe if you intend this"} if {
input.tool == "npx"
input.mode == "read"
not input.failsafe_enabled == false
}
```

Expand Down
2 changes: 1 addition & 1 deletion docs/explanation/why-failsafe.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ See [Design Principles](./design-principles.md) for the full fail-closed discuss

## Read-only by default, per pane

Beyond comprehension, failsafe establishes a mode boundary. Every terminal pane starts in `read` mode: the bundled policies block all mutating verbs across `kubectl`, `helm`, `terraform`, `aws`, and `git`. You flip a pane to `read & write` when you intend to make changes, and it flips back when you close it or reset it.
Beyond comprehension, failsafe establishes a mode boundary. Every terminal pane starts in `enabled` mode: the bundled policies block all mutating verbs across `kubectl`, `helm`, `terraform`, `aws`, and `git`. You flip a pane to `disabled` when you intend to make changes, and it flips back when you close it or reset it.

The per-pane granularity matters: you can run an agent in one pane (armored, read-only) while keeping another agent shell in another pane fully writable. The guard does not get in your way. It gets in the agent's way when the agent is about to do something you have not consciously allowed.

Expand Down
2 changes: 1 addition & 1 deletion docs/how-to/custom-tool-parser.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ gh (user YAML at /Users/you/.config/failsafe/tools/gh.yaml)
A loaded parser alone does not block anything: it only makes facts available. Add a `block` rule to your user policy (`~/.config/failsafe/policy.rego`) to act on the new tool:

```rego
package guard.user
package failsafe.user

import future.keywords.if
import future.keywords.contains
Expand Down
8 changes: 4 additions & 4 deletions docs/how-to/explain-a-command.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,14 @@ Reason : prod is read-only

When a command contains multiple tool calls chained with `&&` or `;`, each call gets its own `── call N ──` block. explain stops at the **first block**, mirroring the hook.

## explain always evaluates in read mode
## explain always evaluates in enabled mode

`explain` is a dry-run of the **default** guard: it always evaluates in `read` mode and
`explain` is a dry-run of the **default** guard: it always evaluates in `enabled` mode and
ignores the pane's mode (and `FAILSAFE_MODE`). That is deliberate: it answers the question
that matters: *would this be blocked by default?*

`read & write` only ever *bypasses* bundled `block` rules, so a command that `explain` shows
as blocked by a bundled rule would be allowed in a write pane (your user and repo policies
`disabled` mode only ever *bypasses* bundled `block` rules, so a command that `explain` shows
as blocked by a bundled rule would be allowed in a disabled pane (your user and repo policies
still apply). To see a live decision under the pane's actual mode, run the command through
your agent so the [`failsafe hook`](../reference/cli.md#hook) path evaluates it. The hook
reads the [mode-source chain](../reference/modes.md); `explain` does not.
Expand Down
8 changes: 4 additions & 4 deletions docs/how-to/per-cluster-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ The value in the **NAME** column becomes `input.kubectl.cluster_name` inside you
mkdir -p ~/.config/failsafe
```

Open `~/.config/failsafe/policy.rego` in your editor. It must declare `package guard.user`:
Open `~/.config/failsafe/policy.rego` in your editor. It must declare `package failsafe.user`:

```rego
package guard.user
package failsafe.user

import future.keywords.if
import future.keywords.in
Expand Down Expand Up @@ -58,7 +58,7 @@ block contains {"reason": "kubectl delete namespace blocked on dev cluster"} if
This is the policy from [`examples/policies/per-cluster.rego`](https://github.com/UndermountainCC/failsafe/blob/main/examples/policies/per-cluster.rego). Copy it as a starting point and adjust cluster names.

!!! note "User layer blocks survive write mode"
`~/.config/failsafe/policy.rego` lives in the user layer and fires even when a pane is in `read & write` mode. Only a repo-level `allow_override` in a trusted `.failsafe.rego` can lift a user-layer block. See [Policy cascade](../explanation/policy-cascade.md).
`~/.config/failsafe/policy.rego` lives in the user layer and fires even when a pane is in `disabled` mode. Only a repo-level `allow_override` in a trusted `.failsafe.rego` can lift a user-layer block. See [Policy cascade](../explanation/policy-cascade.md).

## 3. Validate before relying on it

Expand All @@ -70,7 +70,7 @@ Expected output:

```
✓ parse OK
✓ package: guard.user
✓ package: failsafe.user
✓ rule names: no reserved-rule violations
✓ rule shapes: all block/allow_override return {"reason": ...}
✓ fact-field references: all known
Expand Down
6 changes: 3 additions & 3 deletions docs/how-to/repo-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ In the root of the repository:
touch .failsafe.rego
```

The file must declare `package guard.repo`. This is the only layer that can write `allow_override` rules to loosen a bundled or user-level block.
The file must declare `package failsafe.repo`. This is the only layer that can write `allow_override` rules to loosen a bundled or user-level block.

Here is the policy from [`examples/policies/per-repo-protection.rego`](https://github.com/UndermountainCC/failsafe/blob/main/examples/policies/per-repo-protection.rego) as a starting point:

```rego
package guard.repo
package failsafe.repo

import future.keywords.if
import future.keywords.in
Expand Down Expand Up @@ -57,7 +57,7 @@ The validator checks the package name, rule names, rule shapes (`{"reason": …}

```
✓ parse OK
✓ package: guard.repo
✓ package: failsafe.repo
✓ rule names: no reserved-rule violations
✓ rule shapes: all block/allow_override return {"reason": ...}
✓ fact-field references: all known
Expand Down
Loading
Loading