> ## Documentation Index
> Fetch the complete documentation index at: https://docs.neo.projectdiscovery.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Hooks

> Standing rules Neo cannot talk itself out of, enforced on every matching call

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.

<CardGroup cols={2}>
  <Card title="Keep Neo out of somewhere" icon="ban">
    Block reads or writes under a directory, against a host, or on a particular tool — whichever route it would take.
  </Card>

  <Card title="Ask me first" icon="hand">
    Hold a call until you approve it, for the operations where being wrong is expensive.
  </Card>

  <Card title="Give every task a standing fact" icon="note-sticky">
    Inject context at the start of every run, so nobody has to remember to mention it.
  </Card>

  <Card title="Do not finish until…" icon="flag-checkered">
    Send Neo back when it tries to end a task before something you require has happened.
  </Card>
</CardGroup>

## How a rule runs

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

```mermaid theme={"system"}
flowchart LR
    A["run_command: cat vendor/config.yaml"] --> B[PreToolUse fires]
    B --> C{Matcher match?}
    C -->|No| D[Call proceeds]
    C -->|Yes| E["Action: deny"]
    E --> F[Reason returned]
    F --> G[Neo works around it]
```

<Steps>
  <Step title="The event fires">
    Neo calls `run_command` with `cat vendor/config.yaml`. This raises a **PreToolUse** event carrying the tool name and its arguments.
  </Step>

  <Step title="The matcher is checked">
    Every enabled PreToolUse hook is checked against the call. This one matches on **path**:

    ```json theme={"system"}
    { "path": "vendor/" }
    ```
  </Step>

  <Step title="The action is applied">
    The matcher matched, so its action runs:

    ```json theme={"system"}
    { "type": "deny", "reason": "vendor/ is off limits." }
    ```
  </Step>

  <Step title="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.
  </Step>
</Steps>

## 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.

<Note>
  Listing your rules is the one action that does not need approval. Reading policy changes nothing.
</Note>

## Events

| Event | When it fires | What an action can do |
| :- | :- | :- |
| `PreToolUse` | Before a tool runs | Deny the call, or send it back to be retried with different arguments |
| `PostToolUse` | After a tool succeeds | Attach context to the result |
| `PostToolUseFailure` | After a tool fails | Attach context to the result |
| `PermissionRequest` | When a tool needs approval | Require approval before the call runs |
| `SessionStart` | When a task starts | Add standing context to the run |
| `SubagentStop` | Before a subagent finishes | Send it back with a reason |
| `Stop` | Before Neo finishes | Send it back with a reason |

Settings lists each event under its **When it fires** name.

## 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`.

| Field | Matches on | Use it for |
| :- | :- | :- |
| `path` | Every string in the call, whatever the argument is named, relative or absolute | Anything about a file or directory. One rule covers every route in. |
| `tool` | Exact name or glob, e.g. `github_*` | Narrowing a match you already have |
| `source` | `chat`, `slack`, or `github` | Restricting a rule to where the task came from |
| `args` | One named field, via `equals`, `contains`, `glob`, `in`, or `is_secret` | A genuine field test — a host, a flag, a credential-shaped value |

<Warning>
  **"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.
</Warning>

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:

```json theme={"system"}
{ "event": "PreToolUse", "scope": "user", "matcher": { "path": "vendor/" }, "action": { "type": "deny", "reason": "vendor/ is off limits." } }
```

## 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:

```json theme={"system"}
{ "event": "PostToolUse", "scope": "user", "matcher": { "args": [{ "path": "command", "op": "contains", "value": "install" }] }, "action": { "type": "additional_context", "text": "Check the lockfile diff before committing." } }
```

## 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:

```json theme={"system"}
{ "event": "PostToolUseFailure", "scope": "user", "matcher": { "args": [{ "path": "command", "op": "contains", "value": "deploy" }] }, "action": { "type": "additional_context", "text": "Check rollout status before retrying." } }
```

## PermissionRequest

Good for holding a call until a person approves it.

You say:

> always ask before writing to production

Rule created:

```json theme={"system"}
{ "event": "PermissionRequest", "scope": "user", "matcher": { "args": [{ "path": "target.host", "op": "contains", "value": "prod" }] }, "action": { "type": "ask", "reason": "Writes to production need sign-off." } }
```

```mermaid theme={"system"}
flowchart LR
    A[Matching call] --> B[PermissionRequest fires]
    B --> C[Task suspends]
    C --> D{Your decision}
    D -->|Approve| E[Call runs, task resumes]
    D -->|Reject| F[Neo continues without it]
```

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:

```json theme={"system"}
{ "event": "SessionStart", "scope": "org", "matcher": {}, "action": { "type": "additional_context", "text": "Payments and billing services are internet facing." } }
```

## 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:

```json theme={"system"}
{ "event": "SubagentStop", "scope": "user", "matcher": {}, "action": { "type": "deny", "reason": "Write a summary of what changed before finishing.", "maxDenials": 1 } }
```

## 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:

```json theme={"system"}
{ "event": "Stop", "scope": "org", "matcher": {}, "action": { "type": "deny", "reason": "Run the test suite before finishing.", "maxDenials": 2 } }
```

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

| Scope | Shown in settings as | Who sets it | Who it affects |
| :- | :- | :- | :- |
| **org** | Team | A workspace administrator | Everyone in the workspace |
| **repo** | Repository | A workspace administrator | Everyone working on that repository |
| **user** | Personal | Any user, for themselves | Only that person's tasks |
| **thread** | Conversation | Any user, for the current conversation | Only that conversation |

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

| Action | Does | Meaningful on |
| :- | :- | :- |
| `deny` | Blocks the call and returns a reason | PreToolUse, Stop, SubagentStop |
| `allow` | Lets the call through without further checks | PreToolUse, Stop, SubagentStop |
| `ask` | Requires human approval before the call runs | PermissionRequest |
| `additional_context` | Adds text to the run, or to a result | SessionStart, PostToolUse, PostToolUseFailure |
| `update_input` | Sends the call back to be retried with corrected arguments | PreToolUse |
| `judge` | Defers the decision to a configured policy evaluator | PreToolUse, Stop, SubagentStop |

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.

<AccordionGroup>
  <Accordion title="How update_input actually behaves">
    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.
  </Accordion>

  <Accordion title="What judge does today">
    `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.
  </Accordion>

  <Accordion title="Limits that keep a rule from running away">
    * 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.
  </Accordion>
</AccordionGroup>

## 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.

You can also manage rules yourself under **Settings > Hooks**. The list shows your **Personal** rules and your workspace's **Team** rules, grouped by event, with a Team tag on workspace rules. The switch on each row enables or disables a rule, and its menu lets you view the stored config or delete it. Every member sees Team rules, but only a workspace administrator can change them.

Use **search** and **Filter** to narrow the list by event, by scope (Personal or Team), or by status (Active or Inactive). Filtering runs on the server, so the same filters work through the API:

```bash theme={"system"}
curl 'https://neo.api.projectdiscovery.io/api/v1/hooks?scope=org&enabled=true&search=vendor' \
  --header 'X-Api-Key: YOUR_API_KEY'
```

`scope` takes `user`, `org`, `repo`, or `thread`, and `event` takes any event name above. **Search** matches the stored rule: event, tool, path, action type, and the reason, context, or question text. It does not match settings labels, so search `deny` rather than "Block".

## When a rule does not fire

<AccordionGroup>
  <Accordion title="It matched one tool but not another">
    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.
  </Accordion>

  <Accordion title="It never matches anything">
    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.
  </Accordion>

  <Accordion title="It was created but does nothing">
    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.
  </Accordion>

  <Accordion title="The rule is right but a broader one wins">
    Scopes are additive and deny wins. An organisation rule cannot be loosened by a user or thread rule.
  </Accordion>
</AccordionGroup>

## 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.