plugins/lisa-agy/skills/lisa-parity-safety-net-rules/SKILL.md
View, set, and verify the custom guard rules enforced by Lisa's safety-net PreToolUse Bash hook (parity-safety-net.sh). The consolidated cross-agent equivalent of the upstream safety-net plugin's set-custom-rules + verify-custom-rules skills — manages a project-local list of extended-regex patterns that block destructive shell commands, on Codex, agy, Copilot, Cursor, and Claude.
npx skillsauth add codyswanngt/lisa lisa-parity-safety-net-rulesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Manage the custom guard rules that Lisa's safety-net hook enforces on every
Bash command. The hook (hooks/parity-safety-net.sh, registered as a
PreToolUse matcher on Bash) ships with built-in guards against catastrophic
commands; this skill lets a project view, set, and verify additional
project-specific rules on top of those built-ins.
Lisa-native reimplementation. Upstream 0.9.0 shipped two rule-management skills (
set-custom-rules+verify-custom-rules), which this skill consolidates. Upstream 1.0.6 consolidated them too (intocc-safety-net) and moved custom rules to a JSON rulebook system driven by thenpx cc-safety-net ruleCLI. Lisa deliberately keeps its simpler ERE-lines-file design: the Lisa hook must run identically on Codex, agy, Copilot, Cursor, and Claude without an npx dependency, and a flat regex file is auditable in any of those runtimes. It is reimplemented from scratch against Lisa conventions — it does not port or invoke upstream plugin code.Drift tracking. Pinned to
safety-net@[email protected].scripts/plugin-parity-drift.mjscompares this pin against the upstream version in the plugin cache and flags staleness. Do not port or copy upstream plugin code.2.0.3 review. Upstream 2.0.2–2.0.3 added Kimi Code and Amp installation support and release tooling. Those changes do not alter the upstream rule-management skill or Lisa's project-local ERE rule contract, and Lisa does not currently ship hooks for those runtimes, so no behavior is absorbed here.
2.0.4 review. Method, so the next person can re-run it rather than trust it: hash the sorted (relative path, content) manifest of every rule-bearing directory in the plugin cache under both versions, then diff the trees excluding
dist/,.git/, andnode_modules/. Result —src/(230 files),skills/(1), andhooks/(1) are byte-identical across 2.0.3 and 2.0.4 (srcdigest7e346e205fbd1aa7both sides). The only non-generated differences are theversionstring inpackage.json,kimi.plugin.json, and.claude-plugin/plugin.json, plus a test-harness fix intests/cli/cli-statusline.test.tsthat tolerates EPIPE when an oversized payload is flushed at the bounded reader's cap. Everything else is rebuiltdist/output. No rule, guard family, or skill surface moved, so nothing is absorbed here — and, critically, 2.0.4 does not close the gap recorded below, which therefore carries forward unchanged rather than being dropped at the new pin.Known gap at the 2.0.4 pin. Upstream 2.0.0 rebuilt its engine and added two guard families this hook does not mirror:
secret.*(blocks reading or copying SSH keys,.envfiles, cloud credentials, and coding-CLI credential stores) andrm.git-metadata(blocks deleting the.gitcontrol plane). Both were measured against this hook rather than assumed — see the probe table inparity/plugin-routing/[email protected]. Absorbingsecret.*also needs a matcher change, because this hook is registered onBashonly while upstream also screens file tools. That work is tracked separately inparity/FOLLOWUPS.md§5; do not treat this skill's built-in guard list as covering secret access.
# are
ignored. Matching is case-insensitive (grep -Ei).Resolved in this order:
$SAFETY_NET_RULES_FILE (explicit override), else${CLAUDE_PROJECT_DIR:-$PWD}/.claude/safety-net-rules.txtRULES_FILE="${SAFETY_NET_RULES_FILE:-${CLAUDE_PROJECT_DIR:-$PWD}/.claude/safety-net-rules.txt}"
rm -rf of a filesystem root, $HOME/~, or a top-level wildcard — with
quote-aware boundaries, so wrapper/interpreter forms like
bash -c "rm -rf /" and python -c "… os.system('rm -rf /')" match too.
Path-prefixed spellings (/bin/rm, ./rm) count as rm for every rm
guard.rm -rf target hardening: the cwd itself (././), ..-traversal paths,
home-anchored ~/… paths, absolute paths outside the project (/tmp,
/var/tmp, and $TMPDIR are allowed), $VAR targets other than $TMPDIR,
and any recursive forced delete while the working directory is $HOME.main/master/production/prod/release). --force-with-lease is
intentionally allowed, and so is force-pushing a feature branch (sanctioned
rebase workflow). Acceptable parity divergence: a refspec force-push
(git push origin +main) is not blocked — upstream 1.0.6 allows it too.git reset --hard / git reset --merge while the working tree is dirty
(would discard work). Clean-tree resets are intentionally allowed.git rebase --abort / git rebase --quit while the in-progress rebase
holds human-made conflict resolutions (they would be discarded). A clean or
untouched rebase state stays abortable — an agent-recoverable wedge; the
apply backend and a missing AUTO_MERGE ref fail closed. Deliberate
divergence: upstream blocks rebase --abort unconditionally.git checkout discards: the -- pathspec form (with or without a ref),
-f/--force, --pathspec-from-file, and bare git checkout ..
Branch switching and -b/-B creation stay allowed.git switch --discard-changes / -f/--force.git restore touching the worktree — only git restore --staged <path>
without --worktree is allowed (unstaging is safe).git stash drop / git stash clear (push/pop/list/apply stay allowed).git clean with a force flag and no dry-run — -n/--dry-run anywhere
makes it a safe preview.git branch -D (or -d combined with -f, clustered or split);
safe -d and rename -m stay allowed.git tag -d, git reflog delete, git worktree remove --force.find … -delete, find … -exec rm -rf, and xargs … rm -rf
(plain non-recursive rm on find/xargs output stays allowed).dd of=/dev/…, mkfs … /dev/…, shred.DROP DATABASE/SCHEMA/TABLE, TRUNCATE TABLE.Every git guard sees through leading git global options (-C <path>,
-c <k>=<v>, --git-dir[=…], --no-pager, …), so git -C /path <destructive>
is screened the same as git <destructive>.
Malformed hook input fails closed (exit 2 denies the command). A text-scan
hook cannot exempt display commands, so prose like
echo "docs about rm -rf /" can false-positive — quote-break the string or use
the gh-writer heredoc form (payload is stripped before the guards run). For the
same reason the rm guard treats substitution-wrapping as verdict-neutral (issue
#1982): an executable echo "$(rm -rf /)" is blocked, and so are inert twins the
shell would never expand — a single-quoted echo '$(rm -rf /)' or an escaped
echo "\$(rm -rf /)" — since the scan has no quote-context awareness. The same
quote-break or heredoc workaround applies. Spell that heredoc workaround with a
quote-pair delimiter (gh issue create --body-file - <<'EOF') — only that
spelling reaches the payload-strip path. <<"EOF" and <<\EOF do not, and a
gh … <<\EOF writer now fails closed instead.
Read this before treating the safety net as a wall.
What it is for: stopping accidental catastrophic commands — an agent (or a
human) that reaches for rm -rf /, force-pushes main, drops a table, or writes
to a raw device by mistake. Within that job the guards above are the floor and
they are always on.
What it is not: a security boundary against a deliberate adversary. Two structural reasons, both accepted rather than open bugs:
Why that is tolerable: a here-doc mis-classification degrades to the content
guards rather than bypassing them. Only the narrow gh issue|pr writer form ever
strips payload text; every other path passes the raw command to the guards,
and since issue #1982 those guards see through substitution wrappers. So a
mis-read here-doc costs no guard coverage for any destructive class listed above.
The honest bottom line: an agent that is actively trying to run something destructive has many paths that never involve a here-doc at all — the safety net does not claim to close them. Do not rely on it as the only control over what an agent may execute; treat it as the last line of defence against mistakes, and keep the real boundary (credentials, permissions, environment isolation) outside the agent.
RULES_FILE="${SAFETY_NET_RULES_FILE:-${CLAUDE_PROJECT_DIR:-$PWD}/.claude/safety-net-rules.txt}"
if [ -f "$RULES_FILE" ]; then
echo "Custom safety-net rules ($RULES_FILE):"
grep -vE '^[[:space:]]*(#|$)' "$RULES_FILE" || echo "(no active rules)"
else
echo "No custom rules file yet ($RULES_FILE). Only built-in guards are active."
fi
Read the file to show comments and structure as well.
A rule is an ERE matched against the full command string. Keep rules specific to avoid blocking legitimate work — anchor on the dangerous verb and its target.
Ensure the file exists, then append a commented rule (use Edit/Write,
or append from the shell):
RULES_FILE="${SAFETY_NET_RULES_FILE:-${CLAUDE_PROJECT_DIR:-$PWD}/.claude/safety-net-rules.txt}"
mkdir -p "$(dirname "$RULES_FILE")"
{
echo "# Block deleting a Kubernetes namespace"
echo 'kubectl[[:space:]]+delete[[:space:]]+namespace'
} >> "$RULES_FILE"
Always verify the new rule (next section) before considering it set — confirm it blocks what it should and allows what it shouldn't.
Editing/removing: open the file with Edit and change or delete the line.
Removing a rule never affects the built-in guards.
Two checks — both should pass before you trust a rule.
An invalid regex would make the hook error on every command. Validate it:
printf '%s' "$RULE" | grep -Eq -- "$RULE" 2>/dev/null && echo "valid ERE" \
|| echo "INVALID ERE — fix before saving"
Drive the actual hook with a fake PreToolUse payload and assert the exit
code (non-zero = blocked, 0 = allowed). Build the JSON with jq so the test
command line itself never contains the dangerous literal:
HOOK="${CLAUDE_PLUGIN_ROOT}/hooks/parity-safety-net.sh"
check() { # check <expect: block|allow> <command>
jq -nc --arg c "$2" '{tool_name:"Bash",tool_input:{command:$c}}' \
| bash "$HOOK" >/dev/null 2>&1
local code=$?
local got=allow; [ "$code" -ne 0 ] && got=block
printf '%-5s want=%-5s got=%-5s %s\n' \
"$([ "$got" = "$1" ] && echo OK || echo FAIL)" "$1" "$got" "$2"
}
# Should block (matches the new rule):
check block "kubectl delete namespace prod"
# Should allow (must not over-match):
check allow "kubectl get pods"
Report a table of cases with want/got/verdict. If any case disagrees, tighten the ERE and re-verify.
development
Prepare a machine — a fresh laptop or a throwaway container — to run coding agents, before any repository exists. Detects which of Lisa's supported agents (Claude Code, Codex, Cursor, OpenCode, Antigravity, Copilot) are already installed, asks which credential manager the machine uses (Bitwarden, 1Password, Doppler, Vault, AWS, or none), and installs only what is missing, each by its vendor's own preferred method. Idempotent, headless by default, and emits a Dockerfile for a spin-up/spin-down environment. Run it on a new machine, in a container, or before cloning anything.
tools
Provision and verify a remote execution environment for a host project — Codex Cloud today, other remote surfaces as they are added. Generates a repository-owned setup script that installs the declared toolchain, materializes secrets through lisa-secrets-access, and runs the project's own hook. Provisions by API where one exists, by driving the vendor console where one does not, and by emitting exact config otherwise — then proves the result with the same read-back regardless of which tier did the work. Use before dispatching any work with executionEnv.
tools
Bring a developer's machine in line with the toolchain the project declares. Reports every tool in remoteEnv.tools that is missing, outdated, or unpinned for this platform, and installs the missing ones into ~/.local/bin from the same pinned, checksummed entries the remote surfaces use — but only when asked. Same manifest, same pins, same installers as lisa-setup-remote-env; what differs is consent and that the pin is a floor rather than an equality. Run it on a fresh checkout, after a manifest change, or when a tool fails at the moment of use.
tools
Route one unit of work to a remote execution surface. Reads the executionEnv parameter (local by default, codex-cloud or claude-web today), verifies the environment is provisioned and bound to this repository, submits a thin skill invocation, records the task identifier to .lisa/remote-dispatch.json, and exits without polling. Routing only — the remote runs the identical skill from the identical repository. Composable and inline: other skills invoke it via the Skill tool rather than users calling it directly.