Skip to main content

Failure posture

A permission gate has to decide what happens when it can't do its job. fencepost distinguishes two kinds of failure and treats them oppositely.

FailurePostureWhy
Can't reach a decision for a command (unparseable, or an unexpected error)onError — default askA human is usually present; asking is safe and non-blocking.
The config itself is present but invalidFail closed (deny)Better for a human to notice and fix than to run unguarded.
No config at allDefaults (default: ask)Absence is normal, not an error.

onError: when a command can't be checked

onError: ask   # allow | ask | deny  (default: ask)

This fires when fencepost genuinely cannot evaluate a command — the bash parser can't extract it, or an unexpected exception is thrown mid-evaluation.

  • ask (default) — optimised for interactive use. When fencepost can't check something, you decide.
  • allow — for headless/CI runs where no one can answer a prompt, so an un-checkable command doesn't block the pipeline.
  • deny — the strict option: un-checkable means blocked.
The one silent allow

If stdin isn't valid hook JSON at all, there's no tool, command, or cwd to gate or even to read config from — so there's nothing meaningful to ask about. That single case exits 0 (allow). Everything identifiable goes through onError.

Fail closed on a broken config

A config file that is present but won't parse or is structurally invalid makes fencepost deny every tool call until it's fixed. This is deliberately the opposite of onError: a broken security config is exactly when you want a human in the loop, not silent degradation.

SeverityExamplesEffect
Fatal (fail closed)YAML syntax error; top-level isn't a mapping; invalid default/onError valueDeny everything
Non-fatal (warn, skip rule)One bad rule — invalid regex, missing fieldSkip that rule; the rest applies
Not an errorNo config file presentBuilt-in defaults

The denial carries the offending file and reason, and the SessionStart hook prepends a loud warning so you see the problem immediately rather than only on the next blocked call.

Deadlock by design

While failing closed, every tool is denied — including Read and Write. That means Claude can't edit the config to fix it for you; a human edits the file directly. That's the intended "intercept": a broken gate stops work rather than waving it through.

Verifying config

compileConfig(cwd) is the canonical loader. It returns a report you can inspect from the CLI:

# Print sources, errors, warnings, and the effective config:
fencepost config

# Same report, but exit non-zero if there are errors — for CI / pre-commit:
fencepost verify

Example output:

# Fencepost config

Sources (1):
- /repo/.claude/fencepost.yaml

## Errors (1) — config will FAIL CLOSED until fixed
✖ [/repo/.claude/fencepost.yaml] invalid 'onError' value: "sometimes"
(expected allow|deny|ask)

## Effective config
{ "default": "ask", "onError": "ask", ... }

Wire fencepost verify into a pre-commit hook or CI step so a typo in a security config is caught before it locks anyone out.