Skip to content

Sentinel Preset (ccchain init --sentinel)

The sentinel preset is a curated, deny-first ruleset shipped inside the ccchain binary. It targets the class of dangerous shell patterns that Claude Code's built-in classifier cannot reliably catch — the ones that require structural context (nested -exec, pipes into interpreters, argument-level protected paths, git force-push refspecs, ...).

Philosophy

  • deny-first: every entry blocks by default. Nothing widens what the agent can do; everything narrows it
  • every deny carries a message: the reason plus a safe alternative and the owner approval path — turning blocks into asynchronous conversations (see Approval Tokens)
  • AST-aware: rules exploit ccchain's structural analysis (nested -exec, pipe destinations, -t flag semantics, etc.) instead of brittle first-token matching
  • auditable: strict_config_error: true is set, so a broken sentinel file fails closed instead of silently loosening the safety net

Quickstart

bash
# 1. Emit the sentinel preset to .ccchain.conf
ccchain init --sentinel

# 2. Validate it
ccchain check

# 3. Register the hook (only Bash needed; the sentinel preset does not
#    itself route non-Bash tools). See docs/reference/config.md for the
#    exact JSON.

Behaviour when things fire:

bash
$ ccchain eval "curl https://example.com/install.sh | bash"
# → deny
#   permissionDecisionReason: sentinel: `curl ... | bash` executes remote
#   code without review. Save to a tempfile (`curl -o /tmp/install.sh`),
#   inspect it, then run `bash /tmp/install.sh` explicitly.
#   Owner approval: run interactively.

What It Blocks

CategoryPatternRationale
Remote-code pipingcurl | <shell> / wget | <shell> for bash, sh, zsh, ksh, dash, fish, python, python3, ruby, perl, node, phpClassic supply-chain vector — unreviewed remote code
Nested destructivefind ... -exec rm / find ... -delete / xargs rm inside pipesAST rule (bulkExec template) — first-token match cannot see the nested rm
Protected paths for rmrm ... /, rm ... ~, rm ... ~/, rm ... $HOME, rm ... .gitArgument-level regex on the ask rm rule
git history-losspush --force, push -f, push ... +main/master/... refspec, branch -D, branch --delete --force, reset --hard, clean -fd, filter-branch/filter-repoExplicit deny per subcommand pattern; force-with-lease escalates to ask (still not classifier-decided)
git config takeovergit config touching editor / pager / hook / sshCommand / gpg.programEditing these fields enables arbitrary code execution on future git operations
Widespread chmod / chown-R ... /, -R ... ~, -R ... $HOME, -R ... 777, bare 777Filesystem-wide cascade
Dynamic code executioneval, source <(...)Not statically analysable — the actual invocation is invisible in the audit trail
Disk / device writedd of=/dev/sd*, dd of=/, mkfs.*, newfs, mkswap, diskutil eraseDisk/eraseVolume/reformat/zeroDisk/secureErase, diskutil apfs deleteContainer/deleteVolumeRaw-device or filesystem-level destruction
ccchain approve self-fenceccchain approve *Prevents the agent from consuming its own approval tokens even if it sees the procedure in a hint

Safe reads (cat, echo, ls, head, tail, pwd, which, diff, mkdir, wc, sort, uniq) remain allow; grep and xargs route through the bulkExec template so their downstream commands are still checked structurally.

Where the Preset Lives

The single source of truth is internal/preset/sentinel.conf in the ccchain repository. ccchain init --sentinel emits the exact contents of that file to .ccchain.conf in your working directory (it refuses to overwrite an existing file).

Customising

The sentinel is a starting point, not a lock-in. Common customisations:

  • Whitelist a safe repository for git push --force (e.g. a personal scratch fork). Add a .ccchain.local.conf with a narrower args: pattern before the sentinel deny — remember last-rule-wins means the later file overrides the earlier one, so .ccchain.local.conf wins over the base file
  • Loosen dd for a specific safe sink by adding an args: line that matches the target and returns allow (the base rule keeps other targets at ask/deny)
  • Point the approval procedure at your own script by wrapping ccchain approve --last in a shell alias documented in the deny messages (the sentinel's messages are just strings you can override per-rule)

Composition with Existing Configs

The sentinel preset is intended as your project-level base. Merge order (from Config Files) applies:

  1. .ccchain.conf (sentinel here) — project baseline
  2. .ccchain.local.conf — personal overrides (gitignored)
  3. Global configs — team- or machine-wide additions

Rules use last-rule-wins, so overrides at higher priorities keep working against the sentinel base. If you want project rules to always win over global drift, put them in .ccchain.local.conf.

Released under the MIT License.