All behavior is configured via docker-compose.yml environment variables and/or config.yaml (bind-mounted read-only at /etc/forage/config.yaml or as a directory ./config:/etc/forage:ro).
Reload: after changing config.yaml or environment variables, run docker compose restart. There is no hot reload by design: the config decides which processes (e.g. the browser pool) start, so a restart is the safe way to apply changes. Downtime is 1-2 seconds.
Resolution order: built-in defaults → config.yaml → external prompts.yaml → environment variables.
| Key | Default | Description |
|---|---|---|
host |
0.0.0.0 |
Bind address inside the container. Bridge networking requires 0.0.0.0 (docker-proxy routes to it); the real exposure is controlled by the compose ports: mapping. |
port |
3672 |
HTTP port (T9 of "FORA"). The app reads the config before starting uvicorn, so this is honored without rebuilding. |
workers |
2 |
uvicorn worker processes. |
log_level |
info |
debug | info | warning | error |
In-memory LRU only (lost on restart, by design). The master switch enabled turns everything off; per-section toggles can only disable further.
| Key | Default | Description |
|---|---|---|
enabled |
true |
Master switch. false disables all caching regardless of section toggles. |
max_entries |
500 |
Global LRU cap across both caches. |
search.enabled |
true |
Cache search results. |
search.ttl |
300 |
Search TTL in seconds (5 min). Keeps repeated queries from hammering SearXNG's engines (anti-bot protection). |
extract.enabled |
false |
Extract caching off by default: extraction is always fresh (Firecrawl behavior). A short TTL (e.g. 120) gives fast repeat extraction with bounded staleness. Stores full untruncated content in cache: when an LLM re-fetches a URL with higher max_chars, Forage returns an instant cache hit (<1ms) sliced to the new character limit. |
extract.ttl |
3600 |
Extract TTL in seconds (1 h). Pre-defined so enabling the toggle is enough. |
Bypass per request with the Cache-Control: no-cache header; the response header X-Forage-Cache reports hit|miss|bypass|disabled.
| Key | Default | Description |
|---|---|---|
search_name |
web_search |
Custom tool call name presented in OpenAPI schemas and MCP tools list for web search (e.g. web_search, search_web). |
extract_name |
web_extract |
Custom tool call name presented in OpenAPI schemas and MCP tools list for web extraction (e.g. web_extract, fetch_page). |
include_favicon |
false |
Global toggle: if true, adds favicon URLs to search/extract results and formatted text blocks for models. Can also be overridden specifically under search or extract. |
| Key | Default | Description |
|---|---|---|
searxng_url |
http://searxng:8080 |
Base URL of the SearXNG instance. On Docker, use the service name on the shared network (see docs/SEARXNG.md). |
default_lang |
en-US |
Language passed to SearXNG. |
default_engines |
[google, bing, brave, duckduckgo, qwant] |
Default engine filter sent to SearXNG when no engine is requested by the model (legacy key: engines). |
available_engines |
Auto-discovered from SearXNG (GET /config) |
Active search engines on your SearXNG instance. Auto-discovered dynamically at runtime with fallback to built-in defaults. Not needed in config.yaml unless manually overriding live auto-discovery. |
timeout |
15 |
Timeout in seconds per search request. |
default_limit |
10 |
Default number of search results returned when limit is omitted in requests. |
max_limit |
50 |
Maximum allowed search limit per query. |
max_snippet_chars |
350 |
Max character cap per search result snippet. |
max_total_snippet_chars |
3000 |
Total character cap across all snippets per response. |
citation_style |
site_name |
Citation link style format: site_name ([Site Name](url)), site_name_brackets ([[Site Name]](url)), academic ([[1]](url) - numbered reference), site_name_bold ([**Site Name**](url)), site_name_italic ([*Site Name*](url)), bracket_domain ([domain.com](url)), bracket_title ([Source: Title](url)). |
include_favicon |
null |
Optional override for tools.include_favicon specifically for search responses. |
Forage handles search engine reliability on multiple levels:
- Alias Normalization & Validation: When an LLM requests engines (e.g.
google_search,ddg), Forage resolves aliases and validates againstavailable_engines. If unknown engines are requested, it gracefully falls back tosearch.engineswith an informative warning rather than failing the call. - SearXNG Suspension Tracking: When upstream engines encounter bot blocks or CAPTCHAs, SearXNG suspends them and marks them in
unresponsive_engines. Forage parses these, distinguishes active from failing engines, and surfaces a single-line summary (unresponsive_engines) and guidance warning to prevent LLM retry loops. - Search Caching: Search queries are cached in-memory for 5 minutes (
search.ttl: 300) by default, protecting upstream search providers from being suspended. - Live auto-discovery & fallback: The live engine catalog is probed from SearXNG
GET /config(2 s timeout, cached for 5 min, filtered toenabled: trueengines in thegeneralcategory; media sub-scrapers.images/.audioand translation engines are excluded). Fragile path: if the probe fails (e.g. SearXNG still warming up) or returns no engines, Forage serves a built-inDEFAULT_AVAILABLE_ENGINEScatalog that can name engines your instance does not have (it includesyoutubeandredditwhether or not they are registered). SubsequentPOST /searchcalls then report them inwarnings("ignored unknown engine(s)...") and fall back tosearch.engines. The probe failure is logged at debug level only. Pin the catalog to silence discovery: setsearch.available_enginesorFORAGE_AVAILABLE_ENGINES.
| Key | Default | Description |
|---|---|---|
timeout |
30 |
Total seconds budget per URL (applies to static fetch and browser render). |
max_content_chars |
100000 |
Cap on extracted content size. |
only_main_content |
true |
Strip navigation/ads/footer (trafilatura main-content extraction). |
engine |
trafilatura |
Extract engine: trafilatura (default) or readability. readability runs Mozilla Readability.js inside the browser page (page.evaluate, no Node runtime) and converts the article with markdownify. Use it for product/forum pages where trafilatura's main-content heuristic drops the buybox or comments (Amazon). Can also be set per request or per domain override. |
user_agent |
ForageBot/0.1 (+https://github.com/aldemaroc/forage) |
User-Agent for the static fetch (httpx). |
browser_user_agent |
null (commented) |
User-Agent for the browser (Playwright). When unset, a real Chrome desktop UA is used (never a bot UA; it would be a giveaway). |
respect_robots |
false |
Whether to honor robots.txt. Default is false (do not respect). |
force_render |
false |
Always use the browser for extraction (skip the static attempt). Can also be set per request. |
wait_for |
null |
CSS selector to wait for before extracting (browser mode). |
min_content_chars |
200 |
If static extraction yields less text than this, Forage falls back to the browser. |
raw_content_markdown |
true |
true: raw_content mirrors the clean markdown (Firecrawl-style contract; what Hermes' web_extract_tool reads first). false: raw_content keeps the raw HTML. |
prefer_markdown |
true |
Negotiate Accept: text/markdown on the static fetch. When the server implements markdown negotiation (e.g. via .htaccess / Vary: Accept) and serves text/markdown, Forage uses the body directly as markdown (method: "markdown"), skipping trafilatura conversion. Servers without negotiation ignore the Accept and return HTML, so the normal hybrid flow continues. |
citation_style |
site_name |
Citation link style format for extracted content (same styles as search.citation_style). |
include_favicon |
null |
Optional override for tools.include_favicon specifically for extract responses. |
require_max_chars |
false |
If true, marks max_chars as a required parameter in the web_extract tool schema, prompting LLMs to always supply a character budget per call. |
allow_private_ips |
false |
When true, disables the SSRF guard, allowing extraction of URLs that resolve to private, loopback, or link-local IP addresses (e.g. internal wikis, dashboards, cloud metadata). |
max_document_bytes |
150000000 |
Max bytes for document downloads (PDF/DOCX/XLSX/PPTX/RTF). Documents larger than this are skipped (falls through to the hybrid flow). 150 MB default. |
max_response_bytes |
50000000 |
Max bytes for static HTML responses. Responses larger than this are skipped (falls through to the browser flow). 50 MB default. |
reddit_mirror |
null |
Tier 2 redlib mirror. Set to a self-hosted redlib instance to enable tier 2. All public mirrors are Anubis/Turnstile-blocked; tier 2 is static-only and cannot solve either. Env: FORAGE_REDDIT_MIRROR. |
domain_overrides |
{} |
Per-pattern extraction overrides. The YAML key is a pattern (www-insensitive, case-insensitive): x.com matches the host or any subdomain; .x.com is the same with an explicit leading dot; amazon.* is a wildcard on a host label (fnmatch) that matches the host and any subdomain suffix; reddit.com/r/ requires an exact host plus a path prefix. Supported keys per override: force_render (bool), full_text (bool), engine (str: trafilatura or readability), wait_for (str), url_rewrite (str, format host[/path]), scroll (bool), timeout (int 1-120), network_idle_timeout (int 0-60), challenge_timeout (int 0-120), headers (map of custom request headers), cookies (map of custom session cookies). Request-level force_render/wait_for/timeout/engine are absolute and override the domain override. |
Example: pass authenticated cookies to Reddit for high-throughput sub-second .json pulls, and keep the Amazon buybox via readability:
extract:
domain_overrides:
".amazon.*": # all Amazon TLDs + subdomains
force_render: true # price arrives via JS after load
engine: readability # Readability.js keeps the buybox AND returns structured markdown
reddit.com: # 3-tier pipeline with optional session cookies
engine: readability
timeout: 30
cookies:
reddit_session: "..."
token_v2: "..."
youtube.com:
force_render: true
scroll: true # comments mount on scroll1. Documents (pdf/docx/xlsx/pptx/rtf) → raw byte extraction (no browser)
2. Reddit URLs → 3-tier pipeline:
Tier 1: .json API (sub-second, structured markdown)
Tier 2: Redlib / SafeReddit mirror
Tier 3: Browser + Readability (full comment tree)
3. Domain override force_render | request force_render → browser
4. Static fetch (httpx)
5. Status 401/403/429 → browser
6. HTML looks like a SPA (#root, __NEXT_DATA__, ...) → browser
7. Extracted text < min_content_chars → browser
8. Otherwise → deliver static result
Reddit note: Tier 1 (reddit.com/.json) is only consistently efficient with a logged-in session (the reddit_session / token_v2 cookies in the domain_overrides example above, or the FORAGE_REDDIT_SESSION / FORAGE_REDDIT_TOKEN_V2 env vars). Without cookies, Reddit usually answers 403/429 to the .json endpoint; the call then falls to Tier 2 (Redlib mirror) and/or Tier 3 (stealth browser + Readability). That graceful degradation is intended; for sub-second thread pulls, set the session cookie.
| Key | Default | Description |
|---|---|---|
engine |
scrapling |
Browser backend engine: scrapling (default; StealthyFetcher fingerprint impersonation and Cloudflare bypass), playwright, patchright, or obscura (external Rust/V8 headless browser via CDP). |
cdp_url |
"" |
CDP endpoint URL when engine: obscura (e.g. http://127.0.0.1:9223 or ws://127.0.0.1:9223). Ignored for other engines. |
min_idle |
1 |
Warm browser instances kept idle in pool. On scrapling, pool size is fixed at 1. |
max_instances |
5 |
Upper limit of concurrent browser instances. |
idle_timeout |
60 |
Idle seconds before closing an unused browser instance. |
headless |
true |
Run headless. |
launch_timeout |
30 |
Seconds allowed for browser launch before failing. |
stealth |
true |
Apply anti-detection patches (navigator.webdriver masking, realistic Chrome UA, platform flags). |
network_idle_timeout |
5 |
Max seconds to wait for network activity to settle. Set lower for fast SPAs, higher for heavy dashboards. Override per-domain via network_idle_timeout. |
scroll_steps |
0 |
Number of smooth scroll passes down the page before extracting. Essential for sites that lazy-load content or comments via IntersectionObserver (e.g. Reddit, YouTube). 0 disables scrolling (fastest). Override per-domain via scroll: true. |
challenge_timeout |
15 |
Max seconds to wait for an anti-bot challenge (Cloudflare, DDoS-GUARD) to resolve before extracting. Active only under engine: scrapling. Override per-domain via challenge_timeout. |
solve_cloudflare |
false |
Scrapling engine only: use Scrapling's built-in Cloudflare solver on EVERY page. Adds ~5s per page of networkidle wait time. Keep false (default) to let the lightweight title-polling loop in page_action handle challenges with zero overhead on clean pages; set true only if targeting sites with interactive turnstile checkboxes. |
fallback_solver |
true |
Scrapling engine only: when looks_like_challenge() detects a challenge after normal extraction, automatically retry that single URL with Scrapling's built-in challenge solver. Gives automatic bypass without paying the ~5s solver overhead on clean pages. |
| Key | Default | Description |
|---|---|---|
enabled |
false |
Require Authorization: Bearer <key> on /search, /extract and /admin/*. /health stays open (healthcheck). |
Keys come from the FORAGE_API_KEYS env var (comma-separated) and are compared in constant time.
Customize citation rules, tool descriptions, and OpenAPI/MCP parameter descriptions. Supports dynamic template placeholders: {now_date} (e.g. 2026-08-24 03:57 UTC), {year} (2026), {default_engines}, {available_engines}, {default_limit}, {citation_guidelines}.
| Key | Default | Description |
|---|---|---|
prompts_path |
null |
Optional absolute path to an external prompts YAML file (e.g. /etc/forage/prompts.yaml). When set, the file is loaded and merged into the prompts sub-config, overriding built-in defaults. Partial files are fine - missing keys fall back to the built-in values. |
citation_guidelines |
(citation rules block) | Reusable citation rules template. Automatically interpolated into tool descriptions via {citation_guidelines}. |
search_tool_description |
(search template) | Tool description presented in OpenAPI and MCP schemas for web search. |
extract_tool_description |
(extract template) | Tool description presented in OpenAPI and MCP schemas for web extraction. |
search_params |
{query, limit, ...} |
Dictionary of argument descriptions for web search parameters. |
extract_params |
{urls, force_render, ...} |
Dictionary of argument descriptions for web extract parameters. |
You can keep prompts outside config.yaml by pointing prompts_path at a standalone YAML file (see prompts.example.yaml). The env var FORAGE_PROMPTS_CONFIG is an alternative to prompts_path (useful for injecting per-environment prompts without touching the main config file).
Dedicated YouTube search & channel discovery integration. Exposes the youtube_search MCP tool and POST /v1/youtube/search endpoint. Automatically resolves @handles to Channel IDs and provides instant (<40ms) video search via the YouTube Data API v3.
youtube:
api_key: null # Google YouTube Data API v3 key (env FORAGE_YOUTUBE_API_KEY). When set, enables youtube_search.
default_limit: 20 # default video results per query
max_limit: 50 # maximum allowed video results per query| Key | Default | Description |
|---|---|---|
api_key |
null |
Google YouTube Data API v3 key. When set, registers the dedicated youtube_search tool in MCP and OpenAPI schemas and routes queries to the YouTube Data API v3 directly. |
default_limit |
20 |
Default results returned when limit is omitted. |
max_limit |
50 |
Maximum results allowed per request (1-50). |
Activation. The youtube_search tool is active if and only if api_key is set. With a key, queries hit the YouTube Data API v3 directly and @handle resolves to a Channel ID. Without a key the tool is not registered, and any call returns a clear "key required" error.
Environment variables provide the highest precedence in configuration resolution (built-in defaults → config.yaml → external prompts.yaml → environment variables). Any setting passed as an environment variable directly overrides the corresponding value in config.yaml.
| Variable | Section / Target | Purpose & Example |
|---|---|---|
FORAGE_API_KEYS |
auth |
Comma-separated API keys used when auth is enabled (e.g. key1,key2). |
FORAGE_AUTH_ENABLED |
auth.enabled |
Enable or disable API authentication (true/false). |
FORAGE_CONFIG |
Config Loader | Path to YAML config file or mounted directory (default /etc/forage/config.yaml). |
FORAGE_PROMPTS_CONFIG |
prompts.prompts_path |
Standalone prompts YAML path or directory. Overrides prompts.prompts_path. |
FORAGE_SEARXNG_URL / SEARXNG_URL |
search.searxng_url |
Base URL of SearXNG instance (e.g. http://searxng:8080). |
FORAGE_DEFAULT_ENGINES |
search.default_engines |
Comma-separated default engine filter (e.g. google,bing,brave,qwant). Legacy alias: FORAGE_SEARCH_ENGINES. |
FORAGE_AVAILABLE_ENGINES |
search.available_engines |
Comma-separated engine catalog to override live auto-discovery (optional; not needed unless overriding). |
FORAGE_SEARCH_DEFAULT_LANG |
search.default_lang |
Default search language code (e.g. en-US, pt-BR). |
FORAGE_BROWSER_ENGINE |
browser.engine |
Browser engine: scrapling (default), playwright, patchright, or obscura. |
FORAGE_BROWSER_CDP_URL |
browser.cdp_url |
Obscura CDP endpoint URL when engine=obscura (e.g. http://obscura:9223). |
FORAGE_BROWSER_HEADLESS |
browser.headless |
Run browser in headless mode (true/false). |
FORAGE_EXTRACT_ENGINE |
extract.engine |
Extraction engine: trafilatura or readability. |
FORAGE_EXTRACT_MAX_DOCUMENT_BYTES |
extract.max_document_bytes |
Max bytes for document downloads (default 150000000 = 150 MB). Documents larger than this are skipped. |
FORAGE_EXTRACT_MAX_RESPONSE_BYTES |
extract.max_response_bytes |
Max bytes for static HTML responses (default 50000000 = 50 MB). Responses larger than this are skipped. |
FORAGE_REDDIT_MIRROR |
extract.reddit_mirror |
Tier 2 redlib mirror (set to enable tier 2; all public mirrors are Anubis/Turnstile-blocked). Default null (disabled). |
FORAGE_EXTRACT_ALLOW_PRIVATE_IPS |
extract.allow_private_ips |
Allow extraction of URLs resolving to private, loopback, or link-local IPs (true/false). Default false blocks all private/reserved addresses. |
FORAGE_SEARCH_NAME |
tools.search_name |
Custom tool name exposed to LLMs for web search (default web_search). |
FORAGE_EXTRACT_NAME |
tools.extract_name |
Custom tool name exposed to LLMs for web extraction (default web_extract). |
FORAGE_REDDIT_SESSION |
extract.domain_overrides |
Authenticated reddit_session cookie for Reddit JSON API (enclose in quotes "..."). |
FORAGE_REDDIT_TOKEN_V2 |
extract.domain_overrides |
Authenticated token_v2 cookie for Reddit JSON API (enclose in quotes "..."). |
FORAGE_REDDIT_COOKIES |
extract.domain_overrides |
Full raw Reddit cookie string (k=v; k2=v2, enclosed in quotes "...") for Reddit extraction. |
FORAGE_YOUTUBE_API_KEY / YOUTUBE_API_KEY |
youtube.api_key |
Google YouTube Data API v3 key. When set, enables the dedicated youtube_search tool (direct video search & handle resolution). |
FORAGE_YOUTUBE_NAME |
tools.youtube_name |
Custom name exposed to LLMs for the YouTube search tool. |
FORAGE_YOUTUBE_DEFAULT_LIMIT |
youtube.default_limit |
Default number of video results per search. |
FORAGE_YOUTUBE_MAX_LIMIT |
youtube.max_limit |
Maximum allowed video results per search (1-50). |
FORAGE_REQUIRE_MAX_CHARS |
extract.require_max_chars |
Require LLMs to pass max_chars character budgets (true/false). |
FORAGE_CACHE_ENABLED |
cache.enabled |
Master in-memory cache toggle (true/false). |
FORAGE_PORT / PORT |
server.port |
Server HTTP port (default 3672). |
FORAGE_HOST |
server.host |
Server bind host (default 0.0.0.0). |
FORAGE_LOG_LEVEL |
server.log_level |
Logging verbosity (debug, info, warning, error). |
TZ |
System | Container timezone for dynamic prompt date/time injection (e.g. America/Recife). |
| Variable | Where | Purpose |
|---|---|---|
FORAGE_URL |
Hermes .env |
Base URL the Hermes plugin calls (e.g. http://localhost:3672). |
FORAGE_API_KEY |
Hermes .env |
Key the plugin sends when auth is enabled. |
FORAGE_BYPASS_CACHE |
Hermes .env |
true makes the plugin always send Cache-Control: no-cache. |