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.
Both are discovered together and deep-merged. Priority (highest to lowest):
.prowlrc/prowlrc/.prowl.json/prowl.json(project-local, closer-to-cwd wins; Windows uses.\.prowlrc/.\prowlrc)$XDG_CONFIG_HOME/prowl/prowlrcor~/.config/prowl/prowlrc(%XDG_CONFIG_HOME%\prowl\prowlrcor%USERPROFILE%\.config\prowl\prowlrcon 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.
#!/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):
[[ "$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
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.
provider add deepseek \
--type openai-compat \
--base-url "https://api.deepseek.com/v1" \
--api-key "${DEEPSEEK_API_KEY:?set DEEPSEEK_API_KEY}"
models
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 formprowl modelsprints. A missing slash is an error.model addrequires the provider to already exist.model addflags:--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 smallflags:--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 largewith no argument prints the current selection asprovider/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 autoenables per-question difficulty classification withsmall. The classifier can be local or hosted; Auto sends the current prompt to that model and can add cost/latency.- In the TUI,
alt+rselects Auto or a supported concrete effort/Off/On. - The exact lowercase prose word
ultrathinkis 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.
autois conservative and never creates paid Gemini cache resources.offoverrides inherited TTLs but does not delete user-owned cache settings or disable a provider's implicit caching.- Anthropic/Claude supports
5mor1h; Bedrock's1hsupport is model-gated. - Native GPT-5.6+ supports its explicit controls with a
30mminimum 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, default5m; 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.
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
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.
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
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.
lsp add go --command gopls --env GOPATH "$HOME/go"
lsp add typescript --command typescript-language-server --args --stdio
hooks
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 for how hooks
execute (stdin payload, env vars, decisions).
hook add PreToolUse --matcher "^bash$" --command ".prowl/hooks/no-haskell.sh" --name no-haskell
permissions
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
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 falsedisables metrics. - String keys:
data-directory,initialize-as,notifications. - Attribution keys:
attribution-trailer-style(none,co-authored-by,assisted-by) andattribution-generated-with(boolean). - UI settings:
option ui compact BOOL,option ui diff unified|split,option ui transparent BOOL,option ui mouse BOOL(defaulttrue; 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. Useoption reset <key>to wipe inherited values (e.g. aftersource).
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
- When a tool is about to be called, all
PreToolUsehooks with a matchingmatcher(or no matcher) run in parallel. - Duplicate commands are deduplicated — each unique command runs at most once.
- 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)
{
"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:
{ "decision": "allow", "context": "optional context appended to tool result" }
decision:allowto explicitly allow,denyto 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:
{
"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:
---
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 asproject:skill-name. - Add
disable-model-invocation: trueto keep a skill user-only (hidden from the model's available-skills list but still manually invocable).
Environment variables
PROWL_VERSION— exported intoprowlrcat load; the running version (ordevelfor 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:
{
"$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.