docs: document hook deny contract, matcher regex, tool_input keys, and provider config (#10467)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
6621ed24c7
commit
7d6be4a46c
@@ -283,12 +283,20 @@ The built-in OpenAI provider can connect to OpenAI's official API (`api.openai.c
|
||||
Need to connect to multiple OpenAI-compatible endpoints? [Configure custom providers](#configure-custom-provider) instead for easier switching and better organization, as well as custom naming and shareable configurations.
|
||||
:::
|
||||
|
||||
:::note Pointing at a LiteLLM proxy
|
||||
You can reach a [LiteLLM](https://docs.litellm.ai/) proxy in either of two ways—pick one, don't mix them:
|
||||
|
||||
- Use the **OpenAI provider**: set `OPENAI_HOST` to your proxy's root (no trailing path) and `OPENAI_BASE_PATH` to the path it serves (usually `v1/chat/completions`). A `404` usually means `OPENAI_BASE_PATH` is wrong for your proxy. A `401` with `No api key passed in` is a different problem—the API key is not being loaded (for example, a key placed in `config.yaml`, which is ignored); see [Provider API keys and `config.yaml`](/docs/guides/config-files#security-considerations).
|
||||
- Use the dedicated **LiteLLM provider**, which is configured with its own `LITELLM_HOST`, `LITELLM_BASE_PATH`, and `LITELLM_API_KEY` variables instead of the `OPENAI_*` ones.
|
||||
:::
|
||||
|
||||
#### Configuration Parameters
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `OPENAI_API_KEY` | Yes | Authentication key for the API |
|
||||
| `OPENAI_HOST` | No | Custom endpoint URL (defaults to api.openai.com) |
|
||||
| `OPENAI_BASE_PATH` | No | Request path appended to the host (defaults to `v1/chat/completions`). Set this when your endpoint serves the chat completions API at a different path—most proxies expect `v1/chat/completions`, but some are mounted at `chat/completions` (no `v1`). |
|
||||
| `OPENAI_ORGANIZATION` | No | Organization ID for usage tracking and governance |
|
||||
| `OPENAI_PROJECT` | No | Project identifier for resource management |
|
||||
| `OPENAI_CUSTOM_HEADERS` | No | Additional headers to include in the request. Can be set via environment variable, configuration file, or CLI, in the format `HEADER_A=VALUE_A,HEADER_B=VALUE_B`. |
|
||||
|
||||
@@ -182,6 +182,10 @@ Settings are applied in the following order of precedence:
|
||||
|
||||
## Security Considerations
|
||||
|
||||
:::warning Provider API keys do not go in `config.yaml`
|
||||
goose does not read provider API keys from `config.yaml`. A key placed there is ignored, which typically surfaces as an authentication failure such as `No api key passed in`. Store the key in the system keyring (via `goose configure`), or—when using file-based secret storage—in `secrets.yaml`. It can also be supplied through the provider's environment variable (for example `OPENAI_API_KEY`), which takes precedence over stored secrets.
|
||||
:::
|
||||
|
||||
- Avoid storing sensitive information (API keys, tokens) in the config file
|
||||
- Use the system keyring (keychain on macOS) for storing secrets. When available, this is the recommended option.
|
||||
- If goose is using file-based secret storage, secrets are stored in a separate `secrets.yaml` file (in plain text). This can happen when:
|
||||
|
||||
@@ -119,7 +119,7 @@ Place the plugin under a discovered plugin location, such as `~/.agents/plugins/
|
||||
|
||||
| Field | Required | Description |
|
||||
|---|---:|---|
|
||||
| `matcher` | No | Regular expression used to decide whether the rule runs for the event. If omitted, the rule runs for every event of that type. |
|
||||
| `matcher` | No | Regular expression (not a glob) used to decide whether the rule runs for the event. If omitted, the rule runs for every event of that type. |
|
||||
| `hooks` | Yes | Actions to run when the event and matcher apply. |
|
||||
| `type` | No | Action type. goose currently supports `command`. If omitted, `command` is used. |
|
||||
| `command` | Yes for command hooks | Shell command to run. goose runs it with `sh -c`. |
|
||||
@@ -143,7 +143,11 @@ Use `${PLUGIN_ROOT}` in a command to reference the plugin directory. goose also
|
||||
| `BeforeShellExecution` | Before goose runs a shell command | Shell command |
|
||||
| `AfterShellExecution` | After goose successfully runs a shell command | Shell command |
|
||||
|
||||
The matcher is a regular expression matched against the most relevant string for the event. For example, use `"\\.rs$"` to match Rust files on `AfterFileEdit`, or `"^(cargo test|pnpm test)"` to match test commands on `AfterShellExecution`.
|
||||
The matcher is a regular expression matched against the most relevant string for the event. For example, use `"\\.rs$"` to match Rust files on `AfterFileEdit`, or `"^(cargo test|pnpm test)"` to match test commands on `AfterShellExecution`. The match is unanchored, so `"developer__shell"` also matches `"developer__shell_foo"`; anchor with `^`/`$` when you need an exact match.
|
||||
|
||||
:::warning Use `.*`, not `*`, to match everything
|
||||
The matcher is a regular expression, not a glob. A bare `"*"` is an invalid regex, so the whole rule is **silently skipped** (goose logs a warning and moves on). To run a rule for every event, either omit `matcher` entirely or use `".*"`.
|
||||
:::
|
||||
|
||||
:::note
|
||||
`AfterFileEdit` and `AfterShellExecution` only run after successful tool calls. To react to failed edits, failed shell commands, or other failed tool calls, use `PostToolUseFailure`.
|
||||
@@ -211,6 +215,77 @@ tool="$(printf '%s' "$payload" | jq -r '.tool_name // "none"')"
|
||||
echo "goose hook: event=$event tool=$tool" >> "${PLUGIN_ROOT}/hook.log"
|
||||
```
|
||||
|
||||
### Tool Input Keys
|
||||
|
||||
`tool_name` uses the tool's namespaced name (for example `developer__shell`), and `tool_input` holds that tool's own arguments. The keys are the tool's schema, so they vary by tool—a hook that inspects a file path must read the right field for the tool it matched. The keys for goose's built-in `developer` tools are:
|
||||
|
||||
| `tool_name` | `tool_input` keys |
|
||||
|---|---|
|
||||
| `developer__shell` | `command`, `timeout_secs` (optional) |
|
||||
| `developer__write` | `path`, `content` |
|
||||
| `developer__edit` | `path`, `before`, `after` |
|
||||
| `developer__tree` | `path`, `depth` |
|
||||
| `developer__read_image` | `source`, `crop` (optional) |
|
||||
|
||||
For the shell and file tools, `matcher_context` already carries the shell command (on `BeforeShellExecution`/`AfterShellExecution`) or the file path (on `BeforeReadFile`/`AfterFileEdit`), so those hooks can match without parsing `tool_input`.
|
||||
|
||||
## Blocking a Tool Call
|
||||
|
||||
Most events are observation-only: goose runs the hook, logs the result, and continues regardless of what the hook returns. Two events are different—**`PreToolUse` and `Stop` can block**. A hook on any other event (including `UserPromptSubmit`, `PostToolUse`, and the `Before*`/`After*` events) cannot stop anything; a block decision from those events is ignored.
|
||||
|
||||
A `PreToolUse` hook denies the tool call with either of two signals:
|
||||
|
||||
- **Exit code `2`** — goose blocks and takes the reason from **stderr**.
|
||||
- **`{"decision":"block","reason":"..."}` on stdout** — goose blocks and takes the reason from the `reason` field. goose checks stdout whenever the exit code is not `2`, so this signal is honored regardless of whether the hook exits `0` or non-zero.
|
||||
|
||||
For the stdout signal, stdout must start with `{` and `decision` must be exactly `"block"`; any other value allows the call. If the `reason` is empty, goose substitutes `denied by plugin hook`.
|
||||
|
||||
When a `PreToolUse` hook blocks, goose does not run the tool and returns this message to the model:
|
||||
|
||||
```text
|
||||
Tool call denied by policy hook `<plugin>`: <reason>. Do not retry; this is a policy denial, not a transient failure.
|
||||
```
|
||||
|
||||
**A broken hook fails open.** goose blocks only on one of the two deny signals above. If the hook produces neither—it prints nothing or non-`{` stdout and does not exit `2`, or it fails to run at all (a spawn error or a timeout)—the call is logged and allowed. Because the stdout signal is checked independently of the exit code, a hook that prints `{"decision":"block"}` and *then* exits non-zero still blocks; do not rely on a non-zero exit to cancel a block you have already printed.
|
||||
|
||||
A `Stop` hook that blocks forces the turn to keep going instead of ending. To prevent a misbehaving hook from looping forever, goose caps the number of consecutive `Stop` blocks; once the cap is hit, goose overrides the hook and ends the turn. Raise the cap with the `GOOSE_STOP_HOOK_BLOCK_CAP` environment variable.
|
||||
|
||||
### Block a Dangerous Command
|
||||
|
||||
This `PreToolUse` hook blocks any shell command that uses `sudo`:
|
||||
|
||||
```json title="hooks/hooks.json"
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "developer__shell",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${PLUGIN_ROOT}/scripts/block-sudo.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash title="scripts/block-sudo.sh"
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
payload="$(cat)"
|
||||
command="$(printf '%s' "$payload" | jq -r '.tool_input.command // empty')"
|
||||
|
||||
if printf '%s' "$command" | grep -qE '(^|[[:space:]])sudo([[:space:]]|$)'; then
|
||||
printf '{"decision":"block","reason":"sudo is not allowed in this session"}'
|
||||
fi
|
||||
```
|
||||
|
||||
The hook prints nothing when the command is allowed, so goose runs it normally.
|
||||
|
||||
## Examples
|
||||
|
||||
### Notify When a Tool Fails
|
||||
@@ -354,7 +429,7 @@ Check the following:
|
||||
|
||||
### My Hook Timed Out or Failed
|
||||
|
||||
Hook failures are logged but do not crash goose or the tool that triggered the hook. If a hook fails or exceeds its timeout, goose logs the failure and continues.
|
||||
Hook failures are logged but do not crash goose or the tool that triggered the hook. If a hook fails or exceeds its timeout, goose logs the failure and continues. This is the fail-open behavior described in [Blocking a Tool Call](#blocking-a-tool-call): to intentionally stop a tool call, a `PreToolUse` hook must emit a clean block signal, not just exit non-zero.
|
||||
|
||||
Set a larger timeout for long-running hooks:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user