Skip to main content
An instruction is a preference Neo weighs against everything else happening in a task, and can reasonably move past if circumstances call for it. A hook is different: it is a standing rule you set once, and it applies the same way every time a matching situation comes up, without Neo needing to remember it or decide whether to follow it. That difference matters most exactly where instructions are weakest: a long task where earlier context has scrolled away, an ambiguous request, or text in a repository that tries to talk Neo into something. A hook is checked in the execution path, not read out of the prompt, so none of that reaches it.

Keep Neo out of somewhere

Block reads or writes under a directory, against a host, or on a particular tool — whichever route it would take.

Ask me first

Hold a call until you approve it, for the operations where being wrong is expensive.

Give every task a standing fact

Inject context at the start of every run, so nobody has to remember to mention it.

Do not finish until…

Send Neo back when it tries to end a task before something you require has happened.

How a rule runs

Take one concrete call: Neo runs cat vendor/config.yaml through run_command.
1

The event fires

Neo calls run_command with cat vendor/config.yaml. This raises a PreToolUse event carrying the tool name and its arguments.
2

The matcher is checked

Every enabled PreToolUse hook is checked against the call. This one matches on path:
3

The action is applied

The matcher matched, so its action runs:
4

Neo continues

The reason comes back as the tool’s own result, not as a crash. Neo works around it: it tries a different approach, asks you, or moves on to something else.

Creating a rule

Ask Neo in plain language. It proposes the rule, and every create, enable, disable, and delete waits for your explicit approval before it takes effect — a standing rule is not something an agent gets to set for you on its own say-so.
Listing your rules is the one action that does not need approval. Reading policy changes nothing.

Events

Choosing a matcher

The matcher decides whether a rule holds. This is where rules most often go wrong: one that looks right can quietly cover nothing, or cover one route while leaving three others open. The same action reaches a file through several tools, under several argument names — read_file carries path, write_file carries file_path, and run_command hides it inside command.
“Never touch X” belongs on path, not on args. An args condition matches one named field, so a rule written that way catches read_file and silently lets run_command and write_file through. A tool matcher on its own has the same problem in reverse — it leaves every other tool open.
A path ending in a slash means that directory and everything beneath it. vendor/ covers vendor, vendor/lib/x.go, and rm -rf vendor alike. Write vendor/** if you want the contents but not the directory itself. An absent matcher field matches anything; an empty matcher ({}) matches everything within its scope.

PreToolUse

Good for blocking a call before it runs, or sending it back to be retried with different arguments. You say:
never touch anything under the vendor directory
Rule created:

PostToolUse

Good for attaching guidance to a result Neo just produced. You say:
after any dependency install, remind me to check the lockfile diff
Rule created:

PostToolUseFailure

Good for adding guidance when a call fails, the same way PostToolUse does for a success. You say:
when a deploy fails, remind me to check rollout status before retrying
Rule created:

PermissionRequest

Good for holding a call until a person approves it. You say:
always ask before writing to production
Rule created:
The task waits durably: it survives a restart, and resuming carries the same policy it started with. A permission check can only decide whether to ask, so it takes neither deny nor allow. To block a call outright, use a deny on PreToolUse. To stop being asked about something, narrow the rule’s matcher rather than adding a second rule to waive it — a waiver cannot override an approval another rule already requires.

SessionStart

Good for giving every task in a workspace, or for a user, a standing fact to start from. You say:
remember that our payments and billing services are internet facing
Rule created:

SubagentStop

Good for sending a subagent back to finish something before it hands control back. You say:
don’t let a subagent stop without writing a summary of what changed
Rule created:

Stop

Good for holding the task open until something you require has happened. You say:
don’t let a task finish without running the test suite
Rule created:
Neo is told who set the rule, so it treats a send-back as your policy rather than as text that leaked in from somewhere — and it can still explain why it disagrees instead of looping.

Scopes

Scopes are additive: every matching hook is evaluated, and any single deny wins. A narrower scope can never loosen a rule set at a broader one, so a user or thread rule cannot reopen something an org rule denies. Repository rules are enforced like any other, but cannot be created or changed by asking Neo during a task. The repository a task is working on is context, not proof of write access to it — so ask for a user or org rule instead.

Actions

An action paired with an event that does not read it is refused when you create it, and ignored if one is already stored — a rule that could not decide anything should never look like it is enforcing something.
It does not silently rewrite arguments. The call is blocked and Neo is handed the corrected arguments with an instruction to call the tool again using them; that retry then goes through validation and approval like any other call.This matters for approval: arguments are approved before a rule could rewrite them, so a silent rewrite would execute something you never saw. The cost is one extra model round trip, and Neo may choose to do something else instead of retrying.
judge hands the decision to an external policy evaluator. Until one is configured it does not evaluate: an ordinary rule using it is skipped, and a locked organisation rule using it fails closed — it blocks, rather than quietly permitting something an administrator meant to gate.
  • A Stop or SubagentStop denial is bounded by maxDenials, and by a global ceiling across all rules, so no rule can hang a task indefinitely. Each run gets its own budget.
  • Added context is capped in total, shared between SessionStart and post-tool text, so policy cannot crowd out the task.
  • A tool argument too large or too deeply nested to inspect fully is reported rather than silently skipped.
  • Patterns are matched without regular expressions, so no rule can be written that is expensive to evaluate.

Managing rules

Ask Neo directly, the same way you created the rule.
  • “what rules do I have set?” lists them, scoped to what you can see.
  • “disable the vendor rule” turns one off without deleting it.
  • “delete that rule” removes it for good.
Neo shows you what it found before acting on it, and every change waits for your approval.

When a rule does not fire

The matcher is probably an args condition on a field only one tool uses. Move it to path, which is checked against every string in the call.
A path with no wildcard matches only that exact string. Name the directory with a trailing slash (vendor/) to cover it and everything beneath it.
Check the event and action pair. Actions only work on the events that read them — an ask belongs on PermissionRequest, additional_context on SessionStart or the post-tool events.
Scopes are additive and deny wins. An organisation rule cannot be loosened by a user or thread rule.

Good to know

  • A denied tool call does not fail the task. Neo reads the reason back as the tool’s own result and works around it.
  • Hooks apply inside subagents too, not just the main agent, and survive suspend, approval, and resume.
  • A rule set for chat does not apply to a task that arrived from Slack or GitHub unless you say so — use source deliberately.