Imported from vladolaru/claude-code-plugins (
plugins/yoloing-safe/AGENTS.md). Install upstream withnpx skills add vladolaru/claude-code-plugins --skill yoloing-safe. Copyright stays with the author.
yoloing-safe — Agent Instructions
You maintain a dual-host PreToolUse safety hook for Claude Code's YOLO mode
(--dangerously-skip-permissions) and Codex. The hook evaluates tool calls in
this order:
- allowlist
- block-tier rules
- ask-tier rules
- allow
The runtime entrypoint stays at scripts/pre-tool-use-safety.py, but that file
is now a compatibility shim. The assembled rule registry in
scripts/yoloing_safe/rules/__init__.py is the canonical rule order and
metadata source. Claude Code receives ask-tier confirmation output. Codex
payloads include turn_id; because Codex PreToolUse does not support ask,
the runtime converts those outcomes into explicit blocks. Codex also emits
apply_patch plus a patch command instead of Write/Edit plus file_path;
paths.extract_apply_patch_paths() and paths.resolve_tool_path() adapt every
patch target to the canonical Write rules.
Key Files
| File | Role |
|---|---|
scripts/pre-tool-use-safety.py |
Runtime entrypoint and legacy compatibility surface. Re-exports RULES, RULES_BY_TOOL, ALLOWLIST_PATTERNS, DEFAULTS, and helpers for tests and e2e tooling. |
scripts/yoloing_safe/config.py |
Defaults, user config loading, self-protection constants, non-disableable rules. |
scripts/yoloing_safe/context.py |
EvalContext — cached evaluation context shared across detectors per invocation. |
scripts/yoloing_safe/shell.py |
Command normalization, shell tokenization, heredoc stripping, segment splitting. |
scripts/yoloing_safe/paths.py |
Path extraction, sensitive target detection, Bash mutation target collection. |
scripts/yoloing_safe/registry.py |
Declarative detector compilation, custom detector wrapping, and rule builders (block_rule, ask_rule). |
scripts/yoloing_safe/runtime.py |
Main evaluation loop and block/ask/allow output helpers. |
scripts/yoloing_safe/rules/__init__.py |
Canonical ordered rule assembly plus allowlist aggregation. |
scripts/yoloing_safe/rules/*.py |
Domain rule implementations. Add new rules here. |
hooks/hooks.json |
Claude Code hook registration. Do not change the entrypoint path casually. |
tests/test_core.py |
Core compatibility, config, allowlist, and utility tests. |
tests/test_rules_*.py |
Domain rule tests — each file owns its test classes directly. |
tests/test_integration.py |
End-to-end subprocess regression tests. |
tests/test_scenarios.py |
Scenario and evasion regression suites. |
tests/test_meta.py |
Structural invariant checks for rules, allowlists, and scenarios. |
tests/e2e/test-fixtures.json |
Optional e2e overrides. Most rules need no entry. |
CHANGELOG.md |
Version history. Every behavior or shipped architecture change needs an entry. |
Critical Constraints
test-cases.jsonis generated. Edittest-fixtures.jsonor ruleexamples, then runmake generate.- Block-tier rules must stay before ask-tier rules in
scripts/yoloing_safe/rules/__init__.py. - Preserve the public compatibility surface exposed by
scripts/pre-tool-use-safety.pyunless you intentionally update tests, docs, and e2e tooling together.
Public Testing Contract
The shim (scripts/pre-tool-use-safety.py) re-exports RULES, RULES_BY_TOOL, ALLOWLIST_PATTERNS, DEFAULTS, NON_DISABLEABLE_RULES, normalize_command, load_config, is_allowlisted, and strip_writer_heredocs for the test suites, the e2e generator, the benchmark, and the runtime. TestShimCompatContract in tests/test_meta.py is the authority on that list and fails before downstream tooling breaks silently. The e2e generator imports RULES from the package (from yoloing_safe.rules import RULES), so it depends only on the canonical registry.
Rule Structure
Each assembled RULES entry has:
| Key | Required | Description |
|---|---|---|
tier |
yes | "block" or "ask" |
tools |
yes | Tool names the rule applies to |
message |
yes | Guidance for Claude |
examples |
yes | Example commands or file paths for e2e generation |
detect |
custom only | Detector function taking ctx (EvalContext), returning bool or (bool, custom_message) |
patterns |
declarative only | Regex strings, OR semantics |
pattern_groups |
optional | List of AND groups, OR semantics across groups |
require |
optional | Additional regexes that must all match |
exclude |
optional | Regexes that short-circuit the rule |
For declarative rules, registry.py compiles patterns and generates the legacy _detect callable. For custom rules, registry.py wraps the internal detector so tests still see the legacy (detected, message) interface via hook.RULES[rule_id]["_detect"].
Rule Types
Use a declarative rule when the behavior is fully expressible as regex match plus require/exclude conditions. Use the ask_rule() or block_rule() builders from registry.py:
("git_force_push", ask_rule(
tools={"Bash"},
patterns=[r"^git push\b"],
require=[r"(--force\b|-f\b)"],
exclude=[r"--force-with-lease", r"--force-if-includes"],
message="Force push rewrites remote history. Use --force-with-lease instead.",
examples=["git push --force origin hotfix/fix-arena"],
)),
Use a custom rule when you need config lookups, tool-specific path handling, chain-aware logic, or anything procedural. Custom detectors take an EvalContext object:
def detect_my_rule(ctx):
# ctx.command, ctx.tool_name, ctx.tool_input, ctx.config
# ctx.whole_command (cached), ctx.segments (cached)
return bool(_RE_MY_PATTERN.search(ctx.command))
("my_rule", block_rule(
tools={"Bash"},
detect=detect_my_rule,
message="Guidance message.",
examples=["dangerous-command --flag"],
)),
Domain Modules
Each domain module exports an ordered RULE_SPECS list of (rule_id, spec) tuples and an ALLOWLIST_PATTERNS list. The aggregator in rules/__init__.py concatenates them with block-tier first, ask-tier second.
| Module | Domain |
|---|---|
rules/filesystem.py |
Destructive deletion, credential access, zero-access paths, sensitive writes |
rules/git.py |
Push safety, force push, hard reset, discard changes, stash, history rewrite |
rules/network.py |
Data exfiltration, package publishing, SSH destruction, GitHub operations |
rules/system.py |
Permissions, brew, Docker, database, Terraform, inline interpreters |
Testing
CWD matters: Always run pytest from the repo root, not from inside plugins/yoloing-safe/. Integration and scenario tests spawn the hook as a subprocess that inherits CWD. If CWD is inside the plugin directory, relative paths in test commands (e.g., chmod 777 file) resolve under _PLUGIN_ROOT and self-protection blocks them — causing spurious failures. After running make generate (which cds into tests/e2e/), return to the repo root before running pytest.
Fast suites:
pytest plugins/yoloing-safe/tests/ -v
pytest plugins/yoloing-safe/tests/test_meta.py -v
pytest plugins/yoloing-safe/tests/benchmark_hook.py -v
E2E suite (must be run manually by the user — requires interactive TTY for make auth and a Docker daemon):
cd plugins/yoloing-safe/tests/e2e
make generate # regenerate test-cases.json from RULES + test-fixtures.json
make build # build Docker image with latest hook code and fixtures
make auth # interactive — authenticates Claude CLI inside the container
make run # executes all e2e tests (model calls, slow)
# Return to repo root before running pytest
cd /path/to/repo
Agents cannot run make auth or make run — they require an interactive terminal and a valid Claude API session. When e2e-relevant changes are made (new rules, allowlist patterns, test fixtures, Dockerfile), agents should run make generate and make build, then tell the user to run make auth + make run manually.
Which Tests to Run
tests/TESTING.md § Which Tests to Run After Changes maps each kind of change to its suites. Any change under scripts/ runs the whole suite plus benchmark_hook.py; a rule or allowlist change also runs make generate in tests/e2e/ (see After Any Rule Change below).
Rule Workflows
Adding a New Rule
- Choose the domain module in
scripts/yoloing_safe/rules/. - Add regex constants and helpers in that module if needed.
- For custom rules: write a
detect_my_rule(ctx)function. - Add the rule to the module's
RULE_SPECSusingblock_rule()orask_rule(). - If the rule needs a safe variant, add it to that module's
ALLOWLIST_PATTERNS. - Add a
Test{PascalCaseRuleId}class to the matching split suite (test_rules_*.py). - Add scenarios:
- block rule:
scenarios/blocked.jsonand at least onescenarios/evasion.jsonentry - ask rule:
scenarios/asked.json - both: safe variant in
scenarios/allowed.json
- block rule:
- Run the After Any Rule Change steps.
Removing a Rule
- Remove it from the domain module's
RULE_SPECS. - Remove any allowlist entries for that rule.
- Remove scenarios from
blocked.json,asked.json,allowed.json, andevasion.json. - Remove the unit test class from the matching split suite.
- Remove any
tests/e2e/test-fixtures.jsonoverride. - Run the After Any Rule Change steps.
Renaming a Rule
Rename it in all of these places:
- Domain module
RULE_SPECSkey and detector function name if applicable - Module
ALLOWLIST_PATTERNS blocked.json,asked.json,allowed.json,evasion.jsontests/e2e/test-fixtures.json- Test class and any explicit references in tests
- Run the After Any Rule Change steps
Changing a Rule's Tier
- Change the builder from
block_rule()toask_rule()or vice versa. - Move scenario entries between
blocked.jsonandasked.json. - If promoting to block, add evasion scenarios.
- Update the message tone to match the new tier.
- Run the After Any Rule Change steps.
After Any Rule Change
pytest plugins/yoloing-safe/tests/test_meta.py -vpytest plugins/yoloing-safe/tests/ -vpytest plugins/yoloing-safe/tests/benchmark_hook.py -vcd plugins/yoloing-safe/tests/e2e && make generate- Update
CHANGELOG.mdand bump.claude-plugin/marketplace.jsonper the repo versioning rules