From prempti-falco-rules
Authors custom Falco rules for Prempti to control AI coding agents' tool calls like shell commands, file reads/writes, and enforce deny/ask/allow verdicts.
npx claudepluginhub falcosecurity/prempti --plugin prempti-falco-rulesThis skill uses the workspace's default tool permissions.
Write custom Falco rules that govern what AI coding agents can do at runtime. These rules intercept tool calls (shell commands, file writes/reads, MCP calls) and enforce allow/deny/ask verdicts before execution.
Guides creation of markdown-based Hookify rules to block dangerous bash commands, warn on risky file edits, and enforce behavioral guardrails in Claude Code.
Guides authoring Hookify rules to monitor bash commands, file edits, prompts with regex patterns and conditions; covers syntax, events, actions, examples.
Generates PermissionRequest hooks that auto-approve safe operations, auto-deny dangerous ones, and tailor rules to detected project stack. Safer alternative to --dangerouslySkipPermissions for manual permission mode.
Share bugs, ideas, or general feedback.
Write custom Falco rules that govern what AI coding agents can do at runtime. These rules intercept tool calls (shell commands, file writes/reads, MCP calls) and enforce allow/deny/ask verdicts before execution.
Prempti intercepts every tool call a coding agent makes. Each call becomes a Falco event with structured fields. Your rules evaluate these events and decide the verdict:
The agent sees the verdict reason as: "Rule Name: <your output message> | For AI Agents: ... | correlation=<id>". For deny verdicts, the LLM reformulates this for the user. For ask verdicts, the user reads your output message directly in a permission prompt — so write it for a human audience.
Every tool call event exposes these fields for conditions and output:
| Field | Type | Description |
|---|---|---|
correlation.id | u64 | Unique event ID (always > 0, auto-included in output_fields) |
agent.name | string | Agent identifier (e.g., claude_code) |
agent.session_id | string | Session identifier |
agent.cwd | string | Working directory as reported by the agent |
agent.real_cwd | string | Working directory resolved to absolute canonical path |
tool.name | string | Tool name: Bash, Write, Edit, Read, Glob, Grep, Agent, etc. |
tool.use_id | string | Unique identifier for this tool call |
tool.input | string | Full tool input as JSON |
tool.input_command | string | Shell command (Bash tool only, empty otherwise) |
tool.file_path | string | Target file path, raw (Write/Edit/Read only) |
tool.real_file_path | string | Target file path resolved to absolute canonical path (Write/Edit/Read only) |
agent.permission_mode | string | Session permission mode: default, acceptEdits, plan, bypassPermissions (Codex also emits dontAsk) |
agent.transcript_path | string | Session transcript file path (empty when the agent reports null) |
Path fields come in raw/real pairs. Use real_* for policy matching (resolved, absolute). Use raw fields for display.
Every rule needs these fields:
- rule: <Human-readable name>
desc: >
<What this rule does and why>
condition: >
<Boolean expression using fields above>
output: >
<LLM-friendly message starting with "Falco">
priority: <CRITICAL|WARNING|NOTICE|DEBUG>
source: coding_agent
tags: [<verdict tag>]
| Tag | Effect | Priority convention |
|---|---|---|
coding_agent_deny | Block the tool call | CRITICAL or ERROR |
coding_agent_ask | Require user confirmation | WARNING |
(empty []) | Informational / audit only | NOTICE or INFORMATIONAL |
All Falco priorities are valid: EMERGENCY, ALERT, CRITICAL, ERROR, WARNING, NOTICE, INFORMATIONAL (or INFO), DEBUG.
When multiple rules match the same event, verdicts escalate: deny > ask > allow.
The output: field is the message the coding agent (or user) sees. Write it as a clear, self-contained sentence:
%field to interpolate resolved values (e.g., %tool.real_file_path)key=value pairs — those are handled automatically by append_output# Good:
output: >
Falco blocked running sudo because elevated privileges are not permitted
# Bad (structured fields leak into user-facing message):
output: >
Denied | cmd=%tool.input_command correlation=%correlation.id
| Operator | Example |
|---|---|
=, != | tool.name = "Bash" |
contains | tool.input_command contains "rm -rf" |
icontains | tool.input_command icontains "password" — case-insensitive contains |
startswith, endswith | tool.real_file_path startswith "/etc/" |
in | tool.name in ("Write", "Edit") |
pmatch | tool.real_file_path pmatch (sensitive_paths) — prefix match against a list |
glob | tool.real_file_path glob "/home/*/secrets/*" — wildcard pattern matching |
regex | tool.input_command regex "curl.*|.*sh" — RE2 regular expression |
exists | tool.file_path exists — field has a value (cleaner than != "") |
and, or, not | Boolean combinators |
| Transformer | Usage |
|---|---|
val() | Field-to-field comparison: tool.real_file_path startswith val(agent.real_cwd) |
basename() | Extract filename: basename(tool.real_file_path) = ".env" (POSIX split on / — use real_file_path, which the plugin normalizes to forward slashes on every platform) |
tolower() | Case-insensitive comparison: tolower(tool.input_command) startswith "sudo " |
len() | String length: len(tool.input_command) > 1000 — detect anomalous inputs |
Transformers can be chained: basename(tolower(tool.file_path)).
Without val(), the right-hand side is a literal string, not a field reference.
Lists define reusable sets of values. Macros define reusable condition fragments.
- list: my_blocked_commands
items: [rm, mkfs, dd, fdisk]
- macro: is_blocked_command
condition: >
tool.input_command startswith "rm -rf"
or tool.input_command startswith "mkfs"
- rule: Deny dangerous commands
condition: >
tool.name = "Bash"
and is_blocked_command
...
The override key modifies existing rules, macros, and lists across files — essential for customizing defaults without editing them:
# In rules/user/my_overrides.yaml:
# Add paths to the default sensitive_paths list
- list: sensitive_paths
items: [/opt/secrets/]
override:
items: append
# Add conditions to the default is_sensitive_path macro
- macro: is_sensitive_path
condition: or tool.real_file_path contains "/.vault/"
override:
condition: append
# Disable an existing rule
- rule: Monitor activity outside working directory
enabled: false
override:
enabled: replace
# Change a rule's priority
- rule: Ask before writing outside working directory
priority: CRITICAL
override:
priority: replace
# Append an extra condition to an existing rule
- rule: Deny writing to sensitive paths
condition: and not tool.real_file_path startswith "/etc/my-app/"
override:
condition: append
Appendable fields: condition, output, desc, tags, exceptions.
Replaceable fields: all appendable fields plus priority, enabled.
rules/default/coding_agents_rules.yaml — shipped with the project, overwritten on upgraderules/user/*.yaml — preserved across upgrades, this is where custom rules gorules/seen.yaml — DO NOT modify, required for verdict resolutionWhen creating rules for the user, always write to rules/user/.
Before writing a new rule, read rules/default/coding_agents_rules.yaml to check for overlaps. The default ruleset is organized into six sections covering common AI-agent attack surfaces:
/etc/, ~/.ssh/, ~/.aws/, .env files, etc..mcp.json) and slash-command file injection (.claude/commands/).env API base-URL overrides, AI API keys in env filesThe file also exposes reusable lists (sensitive_paths, sensitive_file_names, shell_startup_files, agent_instruction_files, env_file_names, registry_config_files) and macros (is_sensitive_path, is_outside_cwd, is_write_tool, contains_ioc_domain, cmd_contains_ioc_domain) that user rules can extend via override: append.
If the user's request overlaps with an existing rule, prefer extending it via override: append rather than creating a duplicate. If the new rule is more restrictive (e.g., deny where the default only asks), explain the interaction to the user.
After writing a rule, always validate it with Falco. The validation step catches syntax errors, unknown fields, and malformed conditions before the rule goes live.
Check these locations in order. The "installed" locations are written by the platform packagers; the "development build" locations are produced by make falco-* and make download-falco-linux.
Linux / macOS
~/.prempti/bin/falcofalco (if installed globally)build/falco-*-<arch>/usr/bin/falco (Linux, downloaded) or build/falco-*-darwin-<arch>/falco (macOS, source)Windows (PowerShell)
$env:LOCALAPPDATA\prempti\bin\falco.exefalco.exe (rare on Windows — PATH usually points to the installed bin dir via the post-install step)build\falco-0.43.0-windows-<arch>\falco.exe (built via make falco-windows-x64 / falco-windows-arm64)Use the installed config so the plugin is loaded and all fields are recognized.
Linux / macOS
~/.prempti/bin/falco \
-c ~/.prempti/config/falco.yaml \
--disable-source syscall \
-V ~/.prempti/rules/user/my_rules.yaml
Windows (PowerShell)
& "$env:LOCALAPPDATA\prempti\bin\falco.exe" `
-c "$env:LOCALAPPDATA\prempti\config\falco.yaml" `
--disable-source syscall `
-V "$env:LOCALAPPDATA\prempti\rules\user\my_rules.yaml"
This validates:
coding_agent source (catches typos like tool.command instead of tool.input_command)If validation passes, Falco exits 0. If it fails, the error message tells you exactly which rule and which field or expression has the problem.
| Error | Meaning |
|---|---|
LOAD_ERR_COMPILE_CONDITION | Syntax error in condition — undefined macro, invalid field, bad operator |
LOAD_ERR_COMPILE_OUTPUT | Invalid field reference in output template |
LOAD_ERR_YAML_VALIDATE | YAML structure error — missing required field, wrong type |
LOAD_ERR_YAML_PARSE | Malformed YAML — bad indentation, missing quotes, invalid syntax |
LOAD_UNKNOWN_FILTER | Unknown field name — check spelling against the fields table |
LOAD_UNKNOWN_SOURCE | Unknown event source — check for typos in source: (must be coding_agent) |
LOAD_UNUSED_MACRO | Macro defined but not referenced by any rule or other macro |
LOAD_UNUSED_LIST | List defined but not referenced by any rule, macro, or other list |
Warnings must also be fixed. If validation reports LOAD_UNUSED_MACRO or LOAD_UNUSED_LIST, remove the unused macro or list from the file. Do not ship rules with unused definitions.
If neither the installed binary nor a development build is found, validate manually:
source: coding_agent is setval() is used for field-to-field comparisonscoding_agent_deny, coding_agent_ask, or empty []Flag to the user that the rule was not machine-validated.
| Mistake | Why it's wrong | Fix |
|---|---|---|
tool.real_file_path startswith agent.real_cwd | RHS is a literal string "agent.real_cwd", not a field | Use val(): startswith val(agent.real_cwd) |
source: syscall or missing source: | Wrong event source — defaults to syscall | Always set source: coding_agent |
output: "Denied cmd=%tool.input_command id=%correlation.id" | Structured fields leak into user-facing message | Keep output clean; structured fields are in output_fields automatically |
tool.input_command contains "rm" | Matches rm, but also mkdir, chmod, arm64 | Use startswith "rm " or startswith "rm -" for precision |
tags: [deny] | Wrong tag name — broker won't recognize it | Use coding_agent_deny or coding_agent_ask |
Editing rules/default/ or seen.yaml | Overwritten on upgrade / breaks verdict resolution | Write to rules/user/; use override: to modify defaults |
Using tool.input_command without tool.name = "Bash" | tool.input_command is empty for non-Bash tools — condition silently never matches | Always guard with tool.name = "Bash" and ... |
| Creating a rule that overlaps with defaults | User gets unexpected double verdicts or confusion | Read rules/default/ first; extend with override: or explain the interaction |
- rule: Deny destructive shell commands
desc: >
Blocks rm -rf, mkfs, dd, and other destructive commands that could
cause irreversible damage to the filesystem.
condition: >
tool.name = "Bash"
and (tool.input_command contains "rm -rf"
or tool.input_command startswith "mkfs"
or tool.input_command startswith "dd ")
output: >
Falco blocked a destructive command (%tool.input_command)
priority: CRITICAL
source: coding_agent
tags: [coding_agent_deny]
- rule: Ask before network commands
desc: >
Requires user confirmation before the agent runs curl, wget, or
similar network tools that could exfiltrate data.
condition: >
tool.name = "Bash"
and (tool.input_command startswith "curl "
or tool.input_command startswith "wget "
or tool.input_command contains "| curl"
or tool.input_command contains "| wget")
output: >
Falco requires confirmation for a network command (%tool.input_command)
priority: WARNING
source: coding_agent
tags: [coding_agent_ask]
- list: allowed_write_prefixes
items:
- /home/user/myproject/
- rule: Deny writes outside project
desc: >
Restricts file writes to a specific project directory.
condition: >
tool.name in ("Write", "Edit")
and tool.real_file_path != ""
and not tool.real_file_path pmatch (allowed_write_prefixes)
output: >
Falco blocked writing to %tool.real_file_path because it is outside the allowed project directory
priority: CRITICAL
source: coding_agent
tags: [coding_agent_deny]
Match Bash commands by prefix (safer than contains — avoids matching substrings):
condition: tool.name = "Bash" and tool.input_command startswith "sudo "
Match files by name regardless of directory (use real_file_path so basename() works on Windows too):
condition: basename(tool.real_file_path) = "Dockerfile"
Match files inside the working directory (use val() for field comparison):
condition: tool.real_file_path startswith val(agent.real_cwd)
Match files by extension (use endswith):
condition: tool.real_file_path endswith ".key" or tool.real_file_path endswith ".pem"
Combine multiple tools:
condition: tool.name in ("Write", "Edit", "Read") and ...