# Prowl Config

> Use when the user needs help configuring Prowl — writing prowlrc (the Bash config format) or prowl.json, setting up providers, models, LSPs, MCP servers, hooks, skills, permissions, or changing Prowl behavior.

- Skill: `neur0map/prowl-config` (Agent Skill)
- Install (CLI): `npx skillmds@latest add neur0map/prowl-config`
- Raw SKILL.md: https://api.skillmd.com/api/skills/neur0map/prowl-config/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: neur0map (https://skillmd.com/u/neur0map)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/neur0map/prowl-config

---


# Prowl Configuration

Prowl supports two config formats:

- **`prowlrc`** — a Bash script that builds config by calling Prowl builtins.
  **Preferred.** Because it is real Bash you get includes, secrets,
  conditionals, and variables for free.
- **`prowl.json`** — static JSON. Fully supported; see
  [Legacy JSON format](#legacy-json-format).

Both are discovered together and deep-merged. Priority (highest to lowest):

1. `.prowlrc` / `prowlrc` / `.prowl.json` / `prowl.json` (project-local,
   closer-to-cwd wins; Windows uses `.\.prowlrc` / `.\prowlrc`)
2. `$XDG_CONFIG_HOME/prowl/prowlrc` or `~/.config/prowl/prowlrc`
   (`%XDG_CONFIG_HOME%\prowl\prowlrc` or
   `%USERPROFILE%\.config\prowl\prowlrc` on Windows)

Data directories (`~/.local/share/prowl` and `%LOCALAPPDATA%\prowl`) contain
machine-owned JSON state only; Prowl does not discover or execute a `prowlrc`
from those locations.

If a directory has both `prowlrc` and `prowl.json`, they merge (`prowlrc` wins
on conflicts) and Prowl logs a warning.

## prowlrc at a glance

A `prowlrc` is a plain Bash script executed at load time with the same embedded
shell the `bash` tool uses. It builds config by calling builtins (`provider`,
`model`, `mcp`, `lsp`, `hook`, `permissions`, `option`). Statements run top to
bottom; later statements win, and `remove`/`reset` operate on anything defined
earlier or pulled in via `source`.

```bash
#!/usr/bin/env bash
# Includes and secrets are just Bash.
source ~/.config/prowl/shared.sh

provider add anthropic --api-key "$ANTHROPIC_API_KEY"

model large anthropic/claude-sonnet-4-20250514 --max-tokens 16384
model small anthropic/claude-haiku-4-20250514

option skill-path ./skills
permissions allow view ls grep edit
```

Values are ordinary Bash — quote and expand normally (`"$VAR"`, `$(cmd)`,
`${VAR:?required}`). A failing `$(command)` aborts the load.

`PROWL_VERSION` is exported into the script so you can feature-detect the
running Prowl (it is the literal `devel` for local builds):

```bash
[[ "$PROWL_VERSION" != devel ]] && lsp add gopls --command gopls
```

## Commands

All entity commands are verb-first. `remove` accepts `rm` as an alias. Booleans
accept `true/false/1/0/yes/no`, case-insensitive.

### providers

```bash
provider add <id> [flags]    # define/update; repeated calls merge
provider remove <id>         # alias: rm — removes the provider and its models
```

Flags: `--name`, `--type` (`openai`, `openai-compat`, `anthropic`, or a local
type like `ollama`, `lmstudio`, `llamacpp`), `--api-key`, `--base-url`,
`--disable BOOL`, `--flat-rate BOOL`, `--discover-models BOOL`,
`--system-prompt-prefix TEXT`, `--extra-header KEY VALUE` (repeatable),
`--extra-body JSON`, `--provider-options JSON`, `--prompt-cache JSON`.

```bash
provider add deepseek \
  --type openai-compat \
  --base-url "https://api.deepseek.com/v1" \
  --api-key "${DEEPSEEK_API_KEY:?set DEEPSEEK_API_KEY}"
```

### models

```bash
model add <provider>/<id> [flags]      # register a custom model (provider must exist)
model remove <provider>/<id>           # alias: rm
model large [<provider>/<id>] [flags]  # set the large slot; no arg prints it
model small [<provider>/<id>] [flags]  # set the small slot; no arg prints it
```

- `<provider>/<id>` is the same form `prowl models` prints. A missing slash is
  an error. `model add` requires the provider to already exist.
- `model add` flags: `--name`, `--context-window N`, `--default-max-tokens N`,
  `--can-reason BOOL`, `--supports-images BOOL`, `--price-input F`,
  `--price-output F`, `--price-cache-create F`, `--price-cache-hit F`,
  `--reasoning-effort low|medium|high`.
- `model large`/`model small` flags: `--think`, `--reasoning-effort`,
  `--max-tokens N`, `--temperature F`, `--top-p F`, `--top-k N`,
  `--frequency-penalty F`, `--presence-penalty F`, `--provider-options JSON`,
  `--prompt-cache JSON`.
- `model large` with no argument prints the current selection as `provider/id`,
  usable in `$(model large)`.

`large` is the primary coding and conversation-summary model; `small` handles
titles and Auto reasoning classification.

- `model large <provider>/<id> --reasoning-effort auto` enables per-question
  difficulty classification with `small`. The classifier can be local or
  hosted; Auto sends the current prompt to that model and can add cost/latency.
- In the TUI, `alt+r` selects Auto or a supported concrete effort/Off/On.
- The exact lowercase prose word `ultrathink` is highlighted while typing
  and temporarily selects the strongest supported reasoning for that request.
  Code, markup, identifiers, and paths are excluded. It works from any saved
  mode and must never be implemented by changing the persisted preference.

### prompt caching and focus

Providers and model slots accept `--prompt-cache JSON`. Fields are `mode`
(`auto`, `off`, `explicit`), `ttl`, and the optional Gemini
`storage_cost_per_1m_token_hour`. Model fields override provider fields;
omitted fields inherit. `ttl: "auto"` restores the provider default.

- `auto` is conservative and never creates paid Gemini cache resources.
- `off` overrides inherited TTLs but does not delete user-owned cache settings
  or disable a provider's implicit caching.
- Anthropic/Claude supports `5m` or `1h`; Bedrock's `1h` support is model-gated.
- Native GPT-5.6+ supports its explicit controls with a `30m` minimum TTL.
  Earlier native OpenAI retention is model-gated (`in_memory`/`24h`).
  Subscription endpoints do not accept public-API explicit/retention overrides.
- Explicit Gemini caching requires the current billing-plan storage rate.
  TTL is `1m`–`24h`, default `5m`; the full fixed lease is charged once at
  creation and persisted. Never invent a price or call this an invoice.
- Unsupported combinations are rejected. Router affinity does not override
  explicit manual routing. Cache compatibility must not remove reasoning.

```bash
provider add anthropic --prompt-cache '{"mode":"auto","ttl":"1h"}'
model small anthropic/claude-haiku-4-5-20251001 --prompt-cache '{"mode":"off"}'
```

Focus is a separate, opt-in **session** preference, not a global config key.
Use **Focus On/Off** in the TUI or `prowl run --focus` / `--focus=false`.
Omitting the flag preserves the current mode across resume and compaction.
It changes presentation, never requested scope, evidence, or verification.

### mcp

```bash
mcp add <name> --type stdio|sse|http [flags]   # default type is stdio
mcp remove <name>                              # alias: rm
```

Flags: `--command CMD`, `--args ARG` (repeatable), `--env KEY VALUE`
(repeatable), `--url URL`, `--header KEY VALUE` (repeatable), `--timeout N`,
`--disabled BOOL`, `--disabled-tools TOOL` (repeatable), `--enabled-tools TOOL`
(repeatable), `--oauth BOOL`, `--oauth-client-id ID`, `--oauth-client-secret SECRET`,
`--oauth-callback-port PORT`.

```bash
mcp add github --type http \
  --url "https://api.githubcopilot.com/mcp/" \
  --header Authorization "Bearer $GH_PAT"

mcp add filesystem --command node --args /path/to/mcp-server.js
```

### lsp

```bash
lsp add <name> --command CMD [flags]
lsp remove <name>                     # alias: rm
```

Flags: `--args ARG` (repeatable), `--env KEY VALUE` (repeatable),
`--filetypes TYPE` (repeatable), `--root-markers MARKER` (repeatable),
`--timeout N`, `--disabled BOOL`, `--init-options JSON`, `--options JSON`.

```bash
lsp add go --command gopls --env GOPATH "$HOME/go"
lsp add typescript --command typescript-language-server --args --stdio
```

### hooks

```bash
hook add <event> --command CMD [--name NAME] [--matcher REGEX] [--timeout N]
hook remove <event> [--name NAME]    # alias: rm; without --name clears the event
```

Only named hooks can be removed individually — give a hook `--name` if you
intend to remove it later. See [Hooks runtime](#hooks-runtime) for how hooks
execute (stdin payload, env vars, decisions).

```bash
hook add PreToolUse --matcher "^bash$" --command ".prowl/hooks/no-haskell.sh" --name no-haskell
```

### permissions

```bash
permissions allow <tool> [<tool> ...]   # tools that skip permission prompts
permissions deny <tool> [<tool> ...]    # hide tools from the agent entirely
```

`deny` is the inverse of `allow`: it writes `options.disabled_tools`. A denied
tool is hidden from the agent, not merely prompted for.

### options

```bash
option <key> [value]
option reset <list-key>    # clear a list option back to empty
```

- **Boolean keys** (value optional, defaults `true`): `debug`, `debug-lsp`,
  `auto-lsp`, `progress`.
- **Boolean keys phrased positively** (stored as the negated field): `metrics`,
  `auto-summarize`, `provider-auto-update`,
  `default-providers`. Example: `option metrics false` disables metrics.
- **String keys**: `data-directory`, `initialize-as`, `notifications`.
- **Attribution keys**: `attribution-trailer-style` (`none`, `co-authored-by`,
  `assisted-by`) and `attribution-generated-with` (boolean).
- **UI settings**: `option ui compact BOOL`, `option ui diff unified|split`,
  `option ui transparent BOOL`, `option ui mouse BOOL` (default `true`;
  disable to let the terminal/tmux handle selection, copy/paste, and
  scrolling), `option ui scrollbar default|always|never`,
  `option ui exit-banner default|compact|none`,
  `option ui completions-max-depth N`, `option ui completions-max-items N`.
- **List keys** (singular, one value per call, repeatable): `context-path`,
  `global-context-path`, `skill-path`, `disable-skill`. Use `option reset <key>`
  to wipe inherited values (e.g. after `source`).

```bash
option progress false
option skill-path ./skills
option disable-skill prowl-config
option attribution-trailer-style assisted-by
option attribution-generated-with true
option ui compact true
option ui diff unified
option ui exit-banner compact
```

> [!IMPORTANT] These skill paths are loaded by default and do NOT need
> `skill-path`: `.agents/skills`, `.prowl/skills`, `.claude/skills`,
> `.cursor/skills`.

## Hooks runtime

Hooks are user-defined shell commands that fire on agent events. Currently only
`PreToolUse` is supported, which runs before a tool executes. This behavior is
the same however the hook is defined (`hook add` or JSON).

### How hooks work

1. When a tool is about to be called, all `PreToolUse` hooks with a matching
   `matcher` (or no matcher) run in parallel.
2. Duplicate commands are deduplicated — each unique command runs at most once.
3. The hook receives JSON on **stdin** and hook-specific **environment
   variables**.

Event names are case-insensitive and accept snake_case: `PreToolUse`,
`pretooluse`, `pre_tool_use`, `PRE_TOOL_USE` all work.

### Hook input (stdin)

```json
{
  "event": "PreToolUse",
  "session_id": "abc-123",
  "cwd": "/path/to/project",
  "tool_name": "bash",
  "tool_input": { "command": "ls -la" }
}
```

### Hook environment variables

| Variable                     | Description                                       |
| ---------------------------- | ------------------------------------------------- |
| `PROWL_EVENT`                | Event name (e.g. `PreToolUse`)                    |
| `PROWL_TOOL_NAME`            | Name of the tool being called                     |
| `PROWL_SESSION_ID`           | Current session ID                                |
| `PROWL_CWD`                  | Current working directory                         |
| `PROWL_PROJECT_DIR`          | Project root directory                            |
| `PROWL_TOOL_INPUT_COMMAND`   | Value of `command` from tool input (if present)   |
| `PROWL_TOOL_INPUT_FILE_PATH` | Value of `file_path` from tool input (if present) |

### Hook output

**Exit code 0** — hook succeeded. Stdout is parsed as JSON:

```json
{ "decision": "allow", "context": "optional context appended to tool result" }
```

- `decision`: `allow` to explicitly allow, `deny` to block, `none` (or omit).
- `reason`: explanation (used when denying).
- `context`: extra context appended to the tool result.
- `updated_input`: replacement JSON for the tool input; last non-empty wins.

**Exit code 2** — the tool call is blocked; stderr is the deny reason.

**Any other exit code** — non-blocking error; the tool call proceeds.

### Decision aggregation

- **Deny wins over allow** — any deny blocks the call.
- **Allow wins over none** — a lone allow lets it proceed.
- Deny reasons and context strings are concatenated (newline-separated).
- For `updated_input`, the last non-empty value wins.

### Claude Code compatibility

Prowl also accepts the Claude Code hook output format, so existing hooks work
unchanged:

```json
{
  "hookSpecificOutput": {
    "permissionDecision": "allow",
    "permissionDecisionReason": "Auto-approved",
    "updatedInput": { "command": "echo rewritten" }
  }
}
```

## User-invocable skills

Skills can be invoked as commands. Add `user-invocable: true` to the skill's
YAML frontmatter:

```yaml
---
name: my-skill
description: A skill that can be invoked as a command.
user-invocable: true
---
```

- Global skills appear as `user:skill-name`; project skills as
  `project:skill-name`.
- Add `disable-model-invocation: true` to keep a skill user-only (hidden from
  the model's available-skills list but still manually invocable).

## Environment variables

- `PROWL_VERSION` — exported into `prowlrc` at load; the running version (or
  `devel` for local builds).
- `PROWL_GLOBAL_CONFIG` — override global config location.
- `PROWL_GLOBAL_DATA` — override data directory location.
- `PROWL_SKILLS_DIR` — override default skills directory.

## Legacy JSON format

`prowl.json` is the original static format. It still works and merges with
`prowlrc`. Basic structure:

```json
{
  "$schema": "https://raw.githubusercontent.com/neur0map/PROWL/main/schema.json",
  "models": {},
  "providers": {},
  "mcp": {},
  "lsp": {},
  "hooks": {},
  "options": {},
  "permissions": {}
}
```

The `$schema` property enables IDE autocomplete but is optional.

### prowlrc ↔ prowl.json mapping

| prowlrc                             | prowl.json                                             |
| ------------------------------------ | ------------------------------------------------------ |
| `provider add openai --api-key "$K"` | `providers.openai = {"api_key": "$K"}`                 |
| `model add openai/gpt-x --name X`    | append to `providers.openai.models[]`                  |
| `model large openai/gpt-x`           | `models.large = {"provider":"openai","model":"gpt-x"}` |
| `mcp add gh --type http --url U`     | `mcp.gh = {"type":"http","url":"U"}`                   |
| `lsp add go --command gopls`         | `lsp.go = {"command":"gopls"}`                         |
| `hook add PreToolUse --command C`    | append to `hooks.PreToolUse[]`                         |
| `permissions allow view ls`          | `permissions.allowed_tools = ["view","ls"]`            |
| `permissions deny bash`              | `options.disabled_tools = ["bash"]`                    |
| `option skill-path ./skills`         | `options.skills_paths = ["./skills"]`                  |
| `option metrics false`               | `options.disable_metrics = true`                       |
| `option request-timeout 300`          | `options.request_timeout = 300`                        |
| `option attribution-trailer-style none` | `options.attribution.trailer_style = "none"`        |
| `option attribution-generated-with false` | `options.attribution.generated_with = false`       |

### Shell expansion in prowl.json

In JSON, only selected string fields are run through the embedded shell at load
time (in `prowlrc`, everything is native Bash so this table does not apply):

| Surface                                                         | Expansion                          |
| --------------------------------------------------------------- | ---------------------------------- |
| Provider `api_key`, `base_url`, `api_endpoint`, `extra_headers` | yes                                |
| Provider `extra_body`                                           | **no** (JSON passthrough)          |
| MCP `command`, `args`, `env`, `headers`, `url`                  | yes                                |
| LSP `command`, `args`, `env`                                    | yes                                |
| Hook `command`                                                  | runs via `sh -c`, not the resolver |

Supported constructs: `$VAR`, `${VAR}`, `${VAR:-default}`, `${VAR:+alt}`,
`${VAR:?message}`, `$(command)`. An unset variable expands to empty; a failing
`$(command)` is a hard error. A header that resolves to empty is dropped from
the request.

### Security note

Both formats are trusted code. `prowlrc` runs entirely, and any `$(...)` in
`prowl.json` runs at load time, with the invoking user's shell privileges,
before the UI appears. Don't launch Prowl in a directory whose config you
haven't reviewed.

