Stealth Web Search

Configuration

All settings are environment variables. With Docker Compose, put them in a .env file next to compose.yaml (start from .env.example) and run docker compose up -d again. Invalid values stop the server at startup with a message naming the variable. The sub-agents' model can also come from a JSON file, config/models.json: see MODELS.md.

To check the configuration without starting the server, run it in Docker, where it sees .env and config/models.json exactly as the server does:

bash
docker compose run --rm --no-deps stealth-web-search node dist/check-config.js --ping

--ping also checks that the sub-agents' model endpoint answers. Without Docker, run npm run config:check -- --ping; it reads the variables of your shell, and --env-file .env loads a .env file first.

Booleans accept true/false, 1/0, yes/no and on/off. Durations (*_MS) are in milliseconds, at most 2147483647 (about 24.8 days).

HTTP server

VariableDefaultDescription
HOST_PORT8931(Compose only) Host port for the MCP endpoint and dashboard. Always published on 127.0.0.1
HOST127.0.0.1 (0.0.0.0 in Docker)Bind address inside the container or process
PORT8931Listen port
PUBLIC_URLhttp://127.0.0.1:<port>Base URL shown in logs and on the dashboard
AUTH_TOKEN—If set, /mcp requires Authorization: Bearer <token> and the dashboard requires ?token=<token> once (it then sets a cookie). /healthz stays open
ALLOWED_HOSTS—Extra accepted Host header names (DNS-rebinding protection). localhost, 127.0.0.1, [::1] and host.docker.internal are always accepted
DASHBOARD_ENABLEDtrueServe the live dashboard at /
SESSION_IDLE_TIMEOUT_MS1800000Close MCP sessions idle for this long (0 disables). Clients reconnect automatically
MAX_SESSIONS100Maximum concurrent MCP sessions; further initialize requests get HTTP 503
TOOLSETSallTool groups to expose: core, content, forms, tabs, state, debug, capture, agents, scripts. Individual tool names are also accepted. See TOOLS.md

Under Docker Compose, HOST and PORT are fixed by compose.yaml. The server inside the container must bind 0.0.0.0:8931 (the port the image publishes and health-checks), so compose.yaml sets HOST and PORT explicitly and they override anything you put in .env. To change the port you reach the server on, set HOST_PORT (published on 127.0.0.1 only), not PORT. To change the base URL shown in logs and on the dashboard, set PUBLIC_URL in .env — Compose respects it and falls back to http://127.0.0.1:${HOST_PORT} when it is unset. Running the server directly (npm run dev), HOST and PORT work as documented above.

Obscura browser

VariableDefaultDescription
OBSCURA_STEALTHtrueAnti-detection: realistic fingerprint (Chrome on Windows), TLS fingerprint impersonation, navigator.webdriver reported as false, tracker blocking
OBSCURA_PROXY—http://… or socks5://user:pass@host:port for all browser traffic
OBSCURA_USER_AGENT—Custom User-Agent
ALLOW_PRIVATE_NETWORKfalseAllow browsing localhost, RFC 1918 addresses and host.docker.internal. Blocked by default as SSRF protection
OBSCURA_STORAGE_DIR—Persist cookies of the main browser in this directory. Mount a volume there, for example ./data:/data/cookies with OBSCURA_STORAGE_DIR=/data/cookies (on Linux the folder must be writable by uid 1000: mkdir -p data && sudo chown -R 1000:1000 data). Sub-agent and script browsers run on the second engine, which has no storage, so they never see these cookies
OBSCURA_NAV_TIMEOUT_MS30000Maximum time for one navigation, including redirects and JS-triggered navigations
OBSCURA_JS_WATCHDOG_MS30000Stops page JavaScript that runs synchronously for longer than this (0 = off)
OBSCURA_RESTART_ON_CRASHtrueRestart Obscura with backoff if it exits unexpectedly
OBSCURA_SEPARATE_ENGINEtrueRun sub-agent and script browsers on a second Obscura process, so a page that crashes the engine there never resets the main browser. Always on with OBSCURA_STORAGE_DIR (that engine never persists cookies). Not available with OBSCURA_CDP_URL
OBSCURA_LOG_FILTERwarn,obscura=info,obscura_cdp=info,obscura_browser=warnObscura's RUST_LOG filter. Its output appears in our logs as component: obscura-engine
OBSCURA_EXTRA_ARGS—Extra arguments for obscura serve (quotes supported)
OBSCURA_CDP_PORTrandomInternal CDP port (container loopback only). Left unset, the managed engine picks a random free port each start; set it to pin a fixed port
OBSCURA_CDP_URL—Use an Obscura instance you run yourself, for example ws://127.0.0.1:9222/devtools/browser. No process is spawned
OBSCURA_BIN/opt/obscura/obscura in Docker; .obscura/obscura or obscura on your PATH locallyPath to the Obscura binary

Cookies and CSRF. Obscura v0.2.2 does not enforce cross-site request protections the way Chromium does: it attaches SameSite=Strict/Lax cookies to cross-site requests and treats application/json POSTs as "simple" requests (no CORS preflight). So any page the agent visits can make cross-site requests that carry cookies you have given the browser — cookies set with browser_set_cookie, restored with browser_set_storage_state, or persisted across restarts with OBSCURA_STORAGE_DIR. Treat every visited page as untrusted, do not persist sensitive logins with OBSCURA_STORAGE_DIR, and clear cookies (browser_clear_cookies) before sending the agent to untrusted sites.

Build arguments (Docker)

ArgumentDefaultDescription
OBSCURA_VERSIONv0.2.2Obscura release to bundle. Can also be set in .env for Compose
OBSCURA_VARIANTstealthstealth or default (render-only) release archive
OBSCURA_SHA256_ARM64, OBSCURA_SHA256_AMD64sha256 of the v0.2.2 stealth archivesUpdate these when changing version or variant (the digest is on the GitHub release page). An empty value skips verification

Browser behaviour

VariableDefaultDescription
VIEWPORT_WIDTH / VIEWPORT_HEIGHT1280 / 720Viewport of new tabs
ALLOWED_URL_SCHEMEShttp,https,about,dataSchemes the agent may open. file and javascript can never be enabled
TOOL_TIMEOUT_MS120000Upper bound for a single tool call
CDP_COMMAND_TIMEOUT_MS45000Upper bound for a single browser command

Live view

VariableDefaultDescription
LIVE_VIEW_ENABLEDtrueStream the agent's tab to the dashboard while someone is watching
LIVE_VIEW_QUALITY60JPEG quality of streamed frames
LIVE_VIEW_MAX_WIDTH / LIVE_VIEW_MAX_HEIGHT1280 / 720Maximum frame size

Sub-agents

See AGENTS.md. A models file (config/models.json, see MODELS.md) or AGENT_LLM_URL enables the agent_run, agent_automate and agent_find tools. The models file is the recommended way to describe the model. With a models file, the AGENT_LLM_* variables you set override the matching fields of the file (which ones).

VariableDefaultDescription
AGENT_MODELS_FILEconfig/models.json if it exists (/app/config/models.json in Docker)The models file for the sub-agents. none ignores the file. A path that does not exist stops the server. See MODELS.md
AGENT_LLM_URL—OpenAI-compatible chat completions endpoint: a base URL (http://host:8000/v1) or the full …/v1/chat/completions URL. Reached from inside the container: use http://host.docker.internal:<port>/v1 for a model server on your machine
AGENT_LLM_API_KEY—Sent as Authorization: Bearer <key>
AGENT_LLM_MODELfirst listedModel id; default: the first model GET /v1/models lists. With a models file, it selects a model in the file by id, <provider name>/<id> or display name (default: the first model with tool calling)
AGENT_LLM_TEMPERATURE / AGENT_LLM_TOP_P0.4 / 0.95Sampling; none leaves the field out (OpenAI reasoning models only accept their defaults)
AGENT_LLM_MAX_TOKENS_FIELDmax_tokensName of the output-token limit field: max_tokens (vLLM, LM Studio, Ollama, llama.cpp) or max_completion_tokens (OpenAI reasoning models)
AGENT_LLM_REASONING_EFFORTmediumSent as reasoning_effort (low, medium, high, xhigh, …); none leaves it out
AGENT_LLM_THINKING—true/false sends chat_template_kwargs.enable_thinking (Qwen-style templates); empty leaves the model default
AGENT_LLM_STREAMINGtrueStream responses (reasoning appears live on the dashboard; only silence counts towards the timeout)
AGENT_LLM_EXTRA_BODY—JSON object merged into every request, e.g. {"top_k": 20}
AGENT_LLM_TIMEOUT_MS300000Longest silence from the endpoint before a request fails (retried up to 3 times on network errors, 408, 429 and 5xx)
AGENT_CONTEXT_TOKENS65536Context budget of one run; the transcript is compacted to fit. Capped by the model's contextWindow in the models file
AGENT_MAX_OUTPUT_TOKENS8192Output-token limit per model turn (reasoning included), sent in the AGENT_LLM_MAX_TOKENS_FIELD field; must be under half of the context budget. Capped by the model's maxOutputTokens in the models file
AGENT_MAX_STEPS40Default step budget per run (automation runs get at least 50); the host can pass max_steps (up to 200)
AGENT_MAX_RUNTIME_MS900000Wall-clock limit of one run
AGENT_MAX_CONCURRENT2Runs that work at the same time, each with its own browser; more wait in a queue (up to 20)
AGENT_WAIT_SECONDS170How long agent_* tools wait before answering "still running" (the host passes wait_seconds to change it per call)
AGENT_MAX_RESULT_CHARS12000Tool results longer than this are shortened before they reach the model
AGENT_SEARCH_ENGINEduckduckgoweb_search engine: duckduckgo (Bing as fallback) or bing
AGENT_TRANSCRIPTStrueWrite a JSON transcript of every run to LOG_DIR/agent-runs/ (the newest 300 are kept)

Automation scripts

VariableDefaultDescription
SCRIPTS_DIR/data/scripts in Docker (the scripts volume), ./data/scripts locallyWhere agent_automate stores scripts (<name>.js + <name>.json)
SCRIPT_TIMEOUT_MS300000Time limit of one script run (also stops endless loops)
SCRIPT_MEMORY_MB64Memory limit of the script sandbox

Logging

See LOGGING.md for what is logged.

VariableDefaultDescription
LOG_LEVELinfostdout level: trace, debug, info, warn, error, silent
LOG_FILE_LEVELdebugLog file level
LOG_FORMATjson in Docker, pretty in a terminalstdout format
LOG_DIR/app/logs in Docker, ./logs locallyLog directory
LOG_FILE_MAX_SIZE20mRotate when a file reaches this size
LOG_FILE_MAX_FILES14Rotated files to keep
LOG_MAX_STRING_LENGTH2000Truncate long strings in logged payloads (images are replaced by size and hash)
LOG_REDACT_SECRETStrueKeep secrets out of logs and the dashboard: values typed into password- or OTP-like fields, cookie values, session state, and Cookie/Set-Cookie/Authorization headers. The agent still receives everything
LOG_CDP_EVENTStrueLog every CDP event (network, lifecycle, console)