Releases: hoophq/fence
Release list
v1.4.0
Scratch space stops asking 🚧
Agents write to temp directories constantly — build output, checkouts, intermediate files — and clean up after themselves just as often. Fence treated every one of those cleanups as a recursive delete outside the workspace and asked. rm -rf /tmp/agent-build is not a catastrophe, and prompting on it is exactly the false positive that gets a guardrail uninstalled.
Deletes inside a temp directory now pass in silence: /tmp/build, /var/tmp/x, /private/tmp/y, macOS /var/folders/…/T/z, and $TMPDIR/scratch. A prefixed glob like /tmp/build-* still names a specific target, so it counts too.
What still asks
The temp root itself and a bare wildcard sweep of it — rm -rf /tmp, rm -rf /tmp/*, rm -rf $TMPDIR — wipe every process's scratch state rather than one named target, so those keep prompting. So do lookalikes (/tmpfoo/bar), the macOS cache sibling (/var/folders/…/C/x), and traversals that escape the root (/tmp/../etc is really /etc). A temp operand never launders a dangerous one beside it: rm -rf /tmp/scratch ~ is still a deny.
$TMPDIR is resolved to its actual value before the path is judged, so it counts as scratch only when it really points into a temp directory. If TMPDIR is unset — common on Linux — the shell expands rm -rf $TMPDIR/scratch to rm -rf /scratch, and Fence still asks. A project that itself lives under /tmp classifies its own files as workspace-local, not scratch.
New in the rule schema
delete_target and chmod_target accept a new temp value, the least-severe rung of the shared path scale (temp < cwd_relative < outside_workspace < sensitive). Nothing in the recommended pack matches temp, so scratch deletes fall through to the default allow. Match it explicitly to opt back into prompting:
rules:
- id: confirm-temp-deletes
effect: ask
match:
shell:
recursive_delete: true
delete_target: tempSee the match reference for the full scale.
Upgrading
Nothing to do — brew upgrade fence or your usual install path. No config or rulepack changes are required, and no existing rule changes meaning.
Changelog
- 5d9bc6e feat: stop prompting on recursive deletes inside temp dirs
- a740405 fix: resolve
$TMPDIRbefore trusting it as scratch space (an unsetTMPDIRmade$TMPDIR/xmean/x), and rank workspace-local above scratch for projects living under/tmp
Full diff: v1.3.0...v1.4.0
v1.3.0
One hook per agent 🚧
A Claude Code plugin can register a Fence hook of its own — the Hoop plugin ships one. On a machine with both the plugin and a fence init install, Fence evaluated every tool call twice: same verdict, doubled latency, every "🚧 Fence allowed this" notice printed twice.
fence init now detects an enabled plugin that already runs the Fence hook and installs only the status line — the piece no plugin can provide — and removes a duplicate hook an earlier init left behind. Detection deliberately fails toward installing: a false negative merely duplicates work, but a false positive would leave you unguarded, so every uncertainty resolves to "install it". A --global install always writes the hooks, since any single project can disable a plugin out from under it.
New flags
fence init --statusline-only— write just the status line, never the hooksfence init --force-hooks— install Fence's own hook even beside a plugin'sfence uninstall --hooks-only— remove the hooks, keep the status line (plainuninstallstill removes both)
Upgrading
If you run both the Hoop plugin and a fence init install, upgrade and re-run fence init once — it removes the duplicate and keeps your status line. /hoop:doctor (plugin ≥ 0.7.2) now detects real duplicates only and recommends the version-appropriate fix.
Changelog
- ad839ea init: stand down when a Claude Code plugin already runs the Fence hook
- efad8c2 fix:
--globalinit never stands down (a project could disable the plugin and be left unguarded) - f2811e1 fix: reject
--statusline-only/--force-hooksfor agents without a status line - ba403f0 fix: wrapper-script detection requires an actual invocation line, not a mention
- 0e6278a, 607abb9 ci: GitHub→Linear issue sync
Full diff: v1.2.0...v1.3.0
v1.2.0
🚧 The barrier is now always on screen
Fence's proof of protection in Claude Code is no longer a chat message that scrolls away — it's a persistent status line at the bottom of the UI, for the whole session:
🚧 Fence v1.2.0 · 1 pack · 19 rules
If rulepacks fail to load, the line says so instead of showing a quietly lower count (— ⚠️ 1 rulepack failed to load), and if Fence can't load its rules at all, it tells you plainly that tool calls are not being screened. A session without the barrier on screen isn't guarded — now you can tell at a glance, the entire time.
Plays nice with your own status line
The statusLine slot is yours, and Fence never takes it from you:
- Already have a status line — in the target settings, your user-level
~/.claude/settings.json, orsettings.local.json?fence initleaves it untouched and announces through the SessionStart chat banner instead, telling you how to combine the two (append the output offence hook claude-code statuslinefrom your own statusline command). - Configured your own status line after Fence installed its? The next
fence initsteps aside automatically. fence uninstallremoves Fence's status line and never anyone else's.
Upgrading
The status line arrives on your next fence init re-run:
brew upgrade fence # or: npm update -g @hoophq/fence
fence init --global # or per-project: fence initExisting installs keep working unchanged (the SessionStart banner still ships in the binary) — re-running fence init migrates them to the status line. Codex and OpenCode keep their session banners; neither has a status-line surface.
Full Changelog: v1.1.0...v1.2.0
v1.1.0
OpenCode support 🚧
The same rulepacks now guard OpenCode — the third agent, with zero engine changes:
fence init opencode # writes ./.opencode/plugins/fence.js (--global for ~/.config/opencode)OpenCode has no hook protocol, so init generates a tiny plugin that pipes every shell command and file edit to the same engine: bash, edit, write, read, webfetch, and apply_patch (screened per file touched, most severe verdict wins). The session banner confirms Fence is watching, and fence uninstall opencode is the exit door.
Two honest notes:
- OpenCode plugins can block a call but can't show an approval prompt, so
askrules stop the call and route the agent to you for confirmation instead of prompting natively. - Fail-open holds on both sides: if the
fencebinary is missing or errors, the plugin lets the call proceed — a guardrail must never brick the agent it protects.
Also in this release
- The threat-model and architecture docs now describe the hooked surface per agent instead of assuming Claude Code.
Install: brew install hoophq/tap/fence · npm install -g @hoophq/fence · binaries below
Upgrading? The binary alone is enough for existing Claude Code/Codex setups — nothing to re-run. For OpenCode, run fence init opencode once.
Fence 1.0.0 — the compatibility promise
⚠️ Renamed: Leash is now Fence 🚧
Clean break, no compatibility shims. If you had leash installed:
leash uninstall --global # remove the old hooks (with the old binary)
brew uninstall leash # or: npm rm -g @hoophq/leash
brew install hoophq/tap/fence # or: npm install -g @hoophq/fence
fence init --globalState lives in ~/.fence now; project rule files are ./.fence.yaml. Installed packs: re-run fence add <pack>.
What 1.0 promises
1.0 is a compatibility promise, not a feature bar. The three public contracts — the rulepack schema, the registry index format, and the hook envelope — are frozen and versioned for all of 1.x (what that means). Rulepacks declare schema: 1; a pack needing newer vocabulary is refused whole on older binaries (skipped with an "upgrade fence" warning), never half-read.
New since 0.2.0
- Codex support — the same rulepacks now guard OpenAI Codex CLI with zero engine changes:
fence init codex, then trust the hook once via/hooksinside Codex. Shell commands andapply_patchedits are screened per file touched. fence uninstall [agent]— the exit door: removes exactly the hooks init added, preserves everything else.- A written threat model — what Fence defends against, what it deliberately doesn't, and the known evasion paths, honestly.
- Community scaffolding — CONTRIBUTING, SECURITY (private vulnerability reporting), and issue templates; the false-positive template is the one that matters most.
- Windows: honestly unsupported natively —
fence initsays so instead of half-working; WSL works today. #26 tracks native support.
Install: brew install hoophq/tap/fence · npm install -g @hoophq/fence · binaries below
v0.2.0
Allowed-call notices are now on by default
The 🐕 Leash allowed this chat notice — previously opt-in via leash init --verbose — is now the default. It's how you see Leash watching, not just interrupting.
- Nothing to do on upgrade: the new default lives in the hook binary itself, so existing installs pick it up with the binary — no re-run of
leash initneeded. - Prefer the old quiet behavior?
leash init --quiet(per project) orleash init --global --quiet. --verboseremains accepted everywhere for backward compatibility; onleash initit prints a deprecation nudge pointing at--quiet.- Unchanged: allow notices never carry a permission decision, so your own permission settings still apply.
Changelog
- eed4406 Make allowed-call chat notices the default; add --quiet to opt out
v0.1.0
Leash is now visible in the chat
- 🐕 Session banner — every Claude Code session opens with
Leash v0.1.0 is guarding this session (N packs, M rules). If an installed rulepack or.leash.yamlfails to load, the banner says so instead of silently showing a lower count. - Decision notices — deny, ask, and warn decisions post a one-line
🐕notice in the chat naming the rule that fired. Allowed calls stay silent. - Verbose mode —
leash init --verboseannounces allowed calls too. Feedback ridessystemMessageonly; Leash never emits an explicit allow decision, so your own permission prompts still apply. leash initnow converges both hooks in one pass: it heals stale binary paths, toggles--verbose, and preserves anything you added by hand (custom matchers, a--rulesflag).
Upgrading from 0.0.x? The decision notices arrive with the binary alone. To get the session banner, re-run leash init (or leash init --global) once — it adds the new SessionStart hook and leaves the rest of your settings untouched.
Changelog
- 046e12a Make Leash visible in chat: session banner + decision notices
v0.0.4
The rulepack registry 📦
Rulepacks are now a shareable, installable artifact. Someone writes the guardrails once — Terraform, Kubernetes, production databases — and everyone else gets them with one command:
$ leash search
$ leash add terraform-safety
Installed terraform-safety 1.0.0 (5 rules) — active on every tool call from now on.
$ leash check 'terraform destroy'
DENY terraform destroy
rule: terraform-destroy (critical) · from terraform-safetyleash add <pack>— sha256-verified against the registry index before anything touches disk; live immediately in every project, no restart, no per-project setup.leash updateandleash removeround it out.extends:composition — a committed.leash.yamlcan pin a team baseline (extends: [terraform-safety]) on top of your own rules and overrides.- Three packs to start:
terraform-safety,prod-db-guard,k8s-safety— each holding the same near-zero-false-positive line as the built-in pack (terraform plan -destroy,kubectl uncordon, and local psql all stay silent). - Publishing is a PR — add a YAML file + an index entry to the repo and it's live on merge. Self-host with
--registry <url or path>. Authoring & publishing guide →
Hardening that shipped with it: a broken installed pack (or .leash.yaml) is now skipped with a warning instead of taking the whole engine down, leash check names the pack behind every decision, leash update refuses to let one registry replace a pack installed from another, and nothing on the evaluation path can ever touch the network.
Install — brew install hoophq/tap/leash · npm install -g @hoophq/leash — then leash init --global.
v0.0.3
Leash 0.0.3 — your Claude Code hook now survives brew upgrade.
# Homebrew — macOS
brew install hoophq/tap/leash# npm — macOS / Linux
npm install -g @hoophq/leashFixed
- The hook no longer dies on upgrade.
leash initused to resolve symlinks when recording the hook command, so Homebrew installs wrote the version-pinnedCaskroom/leash/<version>/leashpath — and the nextbrew upgradedeleted it, silently disarming Leash (it fails open) until re-init. Init now records the stable/opt/homebrew/bin/leashsymlink, which upgrades repoint. leash initnow heals. If your settings already carry a Leash hook with a stale or different command, re-runningleash initupdates it in place ("Updated the Leash hook command") instead of reporting "already present".
If you installed ≤ 0.0.2 via Homebrew and ran
leash init: runleash init(orleash init --global) once after upgrading — it will repair the hook path for you.
Changelog
- fe47dd4 Write an upgrade-stable path into the Claude Code hook, and heal stale ones
v0.0.2
Leash 0.0.2 — the npm path goes live, and brew install now works first-run on macOS.
# Homebrew — macOS
brew install hoophq/tap/leash# npx — macOS / Linux, no install
npx @hoophq/leashWhat's new
- Smoother macOS install — the Homebrew cask now strips the quarantine attribute in a postflight, so the binary runs immediately after install instead of being killed by Gatekeeper (proper signing + notarization is on the roadmap).
- Shell rc/profile files are treated as env-tier secrets — reading or exfiltrating
~/.zshrc,~/.bashrc,~/.profileand friends now gets the same scrutiny as.envfiles, since rc files routinely export API keys. - npm distribution —
@hoophq/leashships prebuilt binaries for macOS/Linux on x64/arm64 via the esbuild-style optionalDependencies pattern: no postinstall script, no runtime download.