policy
Inspect the per-workspace tool-policy overlay (.lha/policies.jsonl) that gates destructive or sensitive tool calls, and tell the user what to change if they want a rule added or removed.
Inspect per-workspace tool policies
The policies_guard callback consults a JSONL policy file on every tool
call. Policies live in two layers:
- Default seed at
horizon/guardrails/default_policies.jsonl(read-only, ships with the agent). Provides baseline blocks for catastrophic operations (literaldd if=/dev/zero,mkfs, fork bombs,>/dev/sd*,nc -l/ncat -l/socatbind listeners) and credential reads (cat ~/.ssh/id_rsa,cat ~/.aws/credentials). The seed no longer carries brittle substring/regex rules forrm -rforchmod -R— those are now classified by an argv-structural parser (horizon/guardrails/command_safety.py) that lexes the command into tokens and inspects structure instead of pattern-matching raw strings. - User overlay at
.lha/policies.jsonlunder the workspace root. The overlay is appended to the seed — new rules add restrictions on top of the defaults. Mtime-cached, so edits take effect on the next tool call. You can read this file, but you cannot write it:.lha/*and*/.lha/*are themselves hard-denied destructive-path patterns forwriteandedit, and the same paths are hard-denied forbash(append,sed -i,cp/mv,rm,chmodinto.lha/all match a seeddestructive_commands_regexrule). This is by design — seedocs/security-model.md— the agent cannot self-edit its own guard config.
When to use this skill
- The user asks what policies are currently in force.
- The user asks you to block a specific command or path pattern, or to relax (remove) an overlay rule — in both cases you inspect the current file and hand the user the exact change to make; you do not make it yourself.
If the user is asking about a one-time approval for a call that was just
blocked, tell them to type /grant <command> themselves — that slash command
is a user-only surface, not something you can invoke.
File format
One JSON object per line. Blank lines and # comments are skipped.
Every rule must include canonical_tool_name; that field gates which
tool the rule applies to.
A rule may carry one or more of these fields. They are evaluated independently — set as many as you need on a single rule.
| Field | Type | Effect |
|---|---|---|
destructive | true | Always block the tool. |
destructive_commands | {arg: [substring, ...]} | Block when the string arg contains any substring (case-insensitive). |
destructive_commands_regex | {arg: [regex, ...]} | Block when any regex matches the string arg (case-insensitive). Use when substrings are too blunt. Tenant-authored regexes (overlay + grant rules) are validated for length and nested-quantifier patterns before compilation; malformed regexes are skipped with a warning. |
destructive_paths | [prefix, ...] | Block when a path-shaped arg (path, file_path, target_path) starts with any prefix. |
destructive_path_patterns | [fnmatch-glob, ...] | Block when a path-shaped arg matches any fnmatch glob. Use for per-user paths like */.ssh/*. |
Demotable destructive commands (argv classification)
Before the overlay/seed rules run, command_safety.py lexes shell commands
into argv tokens (quote- and operator-aware via stdlib shlex) and inspects
structure. It returns a verdict: "deny" (catastrophic, always blocks),
"ask" (risky, blocks children/headless, prompts the root agent via an
interactive approval card), or None (no opinion). Examples:
- "deny" —
rm -rf /,rm -rf /etc,rm -rf $HOME,rm -rf /*, etc. (recursive force-delete of system/home roots or their subdirectories). - "ask" —
find . -delete,git push --force,chmod -R 777 .,sudo apt install ...,curl <url> | bash.
On an "ask" verdict, the root agent sees an interactive four-button
approval card; the child/headless chain treats it as a hard deny (no
regression in unattended contexts). This replaces the fragile substring/regex
rules that shipped in older seeds — the new seed (default_policies.jsonl)
only carries literal catastrophic commands + credential reads.
Approval modes
The root agent's interactive approval can be set to auto-approve
demotable-ask verdicts (ONLY) via /yolo (toggles between default and yolo
modes). YOLO mode auto-approves the Layer-D interactive ask; it does not
bypass the exfil guard or Layer-C hard-deny rules (catastrophic
- credential reads + seed overlays). Use it when you trust the session and want fewer prompts for risky-but-non-catastrophic operations. Approval mode is per-session state, not persisted.
Tool-narrowing enforcement
Permission rules in .lha/permissions.jsonl or granted via the interactive
approval card may include a commandPrefix, commandRegex, or argsPattern
to narrow blanket allow rules. Overlay and grant rules (source =
"overlay" or "grant") that target bash or process but carry no
such narrowing field are rejected at load time — you cannot grant a blanket
"always allow bash" from the overlay or a session approval; only the
default seed may carry that. This prevents accidental over-granting.
Integration with the permission layer
A policy block returns {"error": ..., "confirmation_required": True}. Tell
the user why the call was blocked; if they want a one-time approval, they
type /grant <command> themselves. To force a prompt (not a hard block) or
block a specific command for the user, use the permission layer instead: add
an ask_user or deny rule to .lha/permissions.jsonl (see
docs/permission-model.md). This overlay is hard-block only, and it lives
under the same .lha/ write restriction as the policy overlay — the user
edits it directly, not you.
Workflow
List active rules
read(".lha/policies.jsonl")
If the file does not exist, the user has no overlay rules yet — only the seed is active. The seed itself lives outside the workspace and is not readable through your tools; describe its coverage from this skill's own "Default seed" section above instead of trying to read it.
The user wants a rule added
Read the current overlay so your suggestion appends correctly, then give the
user the exact line to add themselves — you cannot write .lha/policies.jsonl.
existing = read(".lha/policies.jsonl") # may be missing → treat as ""
new_rule = {"canonical_tool_name": "bash",
"destructive_commands": {"command": ["rm -rf node_modules"]}}
Tell the user: "Add this line to .lha/policies.jsonl (create the file if it
doesn't exist yet, one JSON object per line):" followed by
json.dumps(new_rule).
The user wants a rule removed
Read the overlay, identify the line to drop (0-based index into the overlay,
ignoring blank/comment lines), and tell the user which line number and
content to delete from .lha/policies.jsonl. Confirm the target with them
first — overlay rules typically exist because they (or you on their behalf)
added them to plug a gap.
Worked example
User: "Block npm publish from the terminal."
- Read
.lha/policies.jsonl(returns "" if missing). - Build:
{"canonical_tool_name": "bash", "destructive_commands": {"command": ["npm publish"]}} - Tell the user: "I can't edit
.lha/policies.jsonlmyself — add this line to it (create the file if needed):" followed by the JSON above. "Once saved, the nextnpm publishattempt will be blocked."
Notes
- The default seed is read-only and out of your reach either way — you cannot edit it, and it lives outside the workspace root so you cannot read it through your tools.
- Malformed JSONL fails closed at parse time (the bad line is skipped with a warning) — tell the user to double-check a new rule parses by reading the file back after they save it.
Files of this skill
This skill has no supporting files.