Skip to content

Configuration (config.json)

Every key Jenny reads from config.json, with the default value that actually ships in the code and what changing it does.

Most people never need this page: the Settings screen covers the common choices, and everything it writes ends up here anyway. Come here for the settings that have no UI — Dream, heartbeat, timezone, tool toggles, snapshot retention, model presets — and for exact defaults and ranges.

On Android the file is <data_dir>/workspace/config.json, inside the app’s private storage (<filesDir>/workspace/). It is created on first boot with a minimal skeleton — a gateway.host and a per-install websocket.token_issue_secret — and then filled in by the onboarding wizard.

The Workspace file browser hides config.json by default. That is deliberate: it holds your API keys and the WebUI bootstrap secret, and it is the one file in the workspace that can stop the app from booting. Turn on Developer mode in Settings → System to see it.

Three things to know before you hand-edit it:

  • Changes need an app restart. The only hot-reload path in the app is the WebUI settings screen, which reloads model and provider on the fly after it writes. Nothing watches config.json for external edits.
  • Broken JSON blocks boot. Config load raises on a malformed or invalid file, and the gateway retries three times before giving up — the WebUI never comes online, and there is no in-app editor to fix it from. Keep a copy of a known-good file before editing.
  • Prefer Settings when the setting exists there. The UI validates ranges and writes atomically.

Jenny writes camelCase (apiKey, maxTokens, intervalS), and this page uses camelCase throughout. snake_case is accepted everywhere on read (api_key, max_tokens, interval_s), so a hand-written config in either style loads fine — but a save from the UI rewrites the whole file in camelCase.

Two more parsing rules worth knowing:

  • Unknown keys are ignored silently. A typo in a key name does not raise; the setting simply never applies. If an edit seems to do nothing, check the spelling first.
  • ${VAR_NAME} in any string value is resolved from the environment at startup, in memory only. Resolved values are never written back, so editing through the WebUI preserves the placeholder. A referenced variable that is not set aborts startup with Environment variable 'NAME' referenced in config is not set. On Android there is no practical way to set environment variables for the app process, so this is a desktop/testing feature — on the phone, secrets live in the file (see Security model).

The list of LLM endpoints you configured, plus which one is active. There is no built-in provider catalog: Jenny never infers an endpoint from a model name or key prefix.

{
"providers": {
"providers": [
{
"name": "deepseek",
"format": "openai_compat",
"apiKey": "sk-...",
"apiBase": "https://api.deepseek.com/v1"
}
],
"default": "deepseek"
}
}
Key Type Default Effect
providers.providers[] array [] The configured endpoints. Empty means no agent: the gateway still starts and serves the WebUI, but every turn fails with No provider configured. Add one in settings or config.json.
providers.providers[].name string required Free-form identifier, referenced by providers.default and by modelPresets.<preset>.provider.
providers.providers[].format "openai_compat" | "anthropic" required Selects the wire format. The only field that decides which client is built.
providers.providers[].apiKey string | null null Credential, stored in clear text. Local servers that ignore auth still usually want a placeholder such as "EMPTY".
providers.providers[].apiBase string | null null Full base URL including the version path. When unset: https://api.openai.com/v1 for openai_compat, https://api.anthropic.com for anthropic.
providers.providers[].apiType "auto" | "chat_completions" | "responses" "auto" openai_compat only, and only against a direct api.openai.com base. auto uses Chat Completions and probes the Responses API when a reasoning effort is requested or the model is a known OpenAI reasoning model, with a circuit breaker that stops probing after repeated failures.
providers.providers[].extraHeaders object | null null Headers merged into every request.
providers.providers[].extraBody object | null null Body fields deep-merged into every request.
providers.providers[].extraQuery object | null null Query parameters merged into every request.
providers.default string | null null Name of the active provider. When unset or unmatched, the first entry in the list is used.

The onboarding wizard replaces the entire provider list with the single provider you enter. Details, error strings, and prompt-caching behavior: Providers and models.

Everything about how the agent talks to the model and manages its own context.

Key Type Default Effect
agents.defaults.model string "" Model ID sent to the provider. Empty until onboarding sets it.
agents.defaults.maxTokens int 8192 Max output tokens per request.
agents.defaults.temperature float 0.1 Sampling temperature. Ignored on models where the provider rejects it (Anthropic thinking modes force 1.0; a few Anthropic models omit it entirely).
agents.defaults.reasoningEffort string | null null null lets the model decide. Accepted: low, medium, high — plus two values the Settings dropdown does not offer: none (explicitly disable thinking) and adaptive (Anthropic adaptive thinking, which also enables interleaved thinking between tool calls).
agents.defaults.contextWindowTokens int 65536 Context budget used for prompt building and consolidation decisions. The settings endpoint accepts only 65536 or 262144 and rejects anything else; a hand-edited file can hold other values. If a request still overflows the model’s real context window, the runtime retries (up to 2 times per turn) with a reduced window for the rest of that turn — using the limit parsed from the provider’s error message when it can, otherwise shrinking the window by 50% as a heuristic.
agents.defaults.toolChoice "auto" | "any" | "none" | "required" "auto" How aggressively the model is pushed to call a tool.
agents.defaults.modelPreset string | null null Name of the preset applied at startup. When null, the direct fields above are used.

The three “Advanced parameters” fields in Settings — Max Tokens, Temperature, Reasoning Effort — currently do not save. The UI posts them and shows “Saved!”, but the backend update handler reads only model, default_provider, context_window_tokens, timezone, bot_name, bot_icon and tool_hint_max_length. Editing config.json (or defining a model preset) is the only way to actually change those three today.

Key Type Default Effect
agents.defaults.idleCompactAfterMinutes int ≥ 0 15 Minutes of idle time before the session context is proactively compacted (summary + the most recent messages, rewritten into the session file). 0 disables it, leaving only the token-driven consolidation that never rewrites the file. Accepted under its legacy name sessionTtlMinutes too.
agents.defaults.maxMessages int ≥ 0 120 Upper bound on live messages kept in the session before consolidation kicks in.
agents.defaults.consolidationRatio float 0.1–0.95 0.5 Fraction of the live context consolidated when a consolidation runs.
agents.defaults.dream.enabled bool true Registers the periodic Dream memory-consolidation job at startup.
agents.defaults.dream.intervalH int ≥ 1 2 Hours between Dream runs. The interval restarts on every app launch.
agents.defaults.maxToolIterations int 200 Hard ceiling on tool calls in a single turn.
agents.defaults.maxToolResultChars int 16000 Tool output above this is truncated before it reaches the model.
agents.defaults.contextBlockLimit int | null null Optional cap on context blocks; unset means no extra limit.

dream has exactly two fields — enabled and intervalH. Older docs mentioned cron, modelOverride and maxBatchSize; none of them exist. See Memory and Dream.

Key Type Default Effect
agents.defaults.timezone string "" Empty means auto: the device timezone detected at startup, falling back to UTC only when detection fails. Resolved once per config load, and written back as "" when it still matches the device — so it keeps following the phone. Set an IANA name ("Europe/Rome") to pin it. Drives runtime time context, cron schedules without an explicit tz, and one-shot at times without an offset.
agents.defaults.botName string "Jenny" Assistant name in chat and in the welcome message. Requires a restart to fully apply.
agents.defaults.botIcon string "✿" Emoji shown next to the name. No UI field; restart to apply.
agents.defaults.language string "it" Language for backend-generated text (welcome message and similar). Written once by onboarding from the UI locale. Not the UI language — that lives in the device’s localStorage.
agents.defaults.maxConcurrentSubagents int ≥ 1 1 How many spawned subagents may run at once. Beyond the limit, spawn returns an error so the agent can wait or reorder its work.
agents.defaults.toolHintMaxLength int 20–500 40 Truncation length of the short tool-call hints shown while the agent works.
agents.defaults.disabledSkills string[] [] Skill directory names excluded from the agent’s skill summary, always-on injection, and subagent summaries. Applies to built-in and workspace skills alike.
agents.defaults.providerRetryMode "standard" | "persistent" "standard" Retry policy for provider errors. persistent keeps retrying transient failures longer.
Key Type Default Effect
extractDocumentText bool false When false (the default), non-image attachments are saved under workspace/uploads/ and only referenced by path — the agent reads them on demand with its file tools. Setting it to true restores the legacy behavior of extracting and inlining document text into the prompt on every turn. See Files and attachments.
Key Type Default Effect
gateway.host string "127.0.0.1" Bind address.
gateway.port int 18790 HTTP port.
gateway.heartbeat.enabled bool true Registers the built-in heartbeat cron job at startup. It reads workspace/HEARTBEAT.md and acts only on the ## Active Tasks section.
gateway.heartbeat.intervalS int ≥ 1 1800 Seconds between heartbeat checks (30 minutes). Every cycle that finds a task costs an LLM call.
gateway.heartbeat.keepRecentMessages int 8 Messages retained in the internal heartbeat session after each run.

On the phone, host and port are imposed by the Android runtime. The service calls the gateway entry point with 127.0.0.1:18790 explicitly, which overwrites whatever the file says — both for the HTTP API and for the WebSocket, which share that single port. Editing them in config.json changes nothing on-device; they only matter when running the gateway yourself for local testing.

The heartbeat job is stored like any other cron job (<workspace>/cron/jobs.json) and appears in cron(action="list") as heartbeat, but it is system-managed and cannot be removed with the cron tool — disable it here and restart. See Scheduling and proactivity.

The channel the WebUI talks over. On-device, the runtime forces host and port onto this section too, so the WebView reaches chat and the HTTP API from one origin.

Key Type Default Effect
websocket.enabled bool false Declared, but not consulted by the current wiring: the channel is built whenever a non-empty websocket section exists, and the Android runtime always writes one. Setting it to false does not disable the channel today.
websocket.host string "127.0.0.1" Bind address. Forced to the runtime value on-device.
websocket.port int 8765 Off-device default. On the phone this is always overwritten with 18790.
websocket.path string "/" WebSocket path. Must start with /.
websocket.tokenIssueSecret string "" The install’s shared secret. Generated once at first boot (32 random URL-safe bytes) and persisted here with chmod 600. It authenticates both the WebSocket handshake (?token=…) and the HTTP API (Authorization: Bearer … or X-Jenny-Auth:). Never regenerate it casually — the WebUI receives it through the bootstrap route.
websocket.websocketRequiresToken bool true Requires the token in the handshake.
websocket.allowFrom string[] ["*"] Client-ID allowlist for connections. This is the real key — there is no channels.*.allowFrom anywhere in the codebase.
websocket.streaming bool true Stream assistant text as it is generated.
websocket.sendProgress bool true Send progress events to the WebUI.
websocket.sendToolHints bool false Send the short tool(args…) hints as progress events.
websocket.showReasoning bool true Config-only. Delivers the model’s reasoning stream to the WebUI. Turning it off also stops it being recorded: the transcript append happens inside the same send path the dispatcher gates on this flag, so with it off the reasoning is absent from replay and history too, not just from the live view. Telegram never receives reasoning regardless.
websocket.sendMaxRetries int 0–10 3 Delivery attempts per outbound message, including the first send. Backoff 1 s, 2 s, 4 s, then capped at 4 s.
websocket.maxMessageBytes int 1024–41943040 37748736 Max inbound frame size (36 MiB), sized for four ~6 MB images after client-side normalization plus base64 overhead.
websocket.pingIntervalS float 5–300 20.0 Keepalive ping interval.
websocket.pingTimeoutS float 5–300 20.0 Keepalive ping timeout.
websocket.sslCertfile / websocket.sslKeyfile string "" Optional TLS material for off-device deployments.

One validation to be aware of: setting host to 0.0.0.0 or :: with an empty tokenIssueSecret is rejected at load — the config raises rather than exposing an unauthenticated agent on every interface. Wire-level details for integrators: WebSocket protocol.

Key Type Default Effect
telegram.enabled bool false Master switch. The channel starts only when this is true and a bot token is present.
telegram.botToken string | null null BotFather token, stored in clear text. The API never returns it in full — the settings payload carries only a first4...last4 hint.
telegram.botUsername string | null null Cached bot username from getMe.
telegram.pairedChatId string | null null The paired chat. Its presence is the “paired” state — there is no stored enum.
telegram.pairedUsername string | null null Display name of the paired user.
telegram.pairingCode string | null null The 6-digit code, persisted so pairing survives the frequent process restarts on Android, and cleared once pairing succeeds.
telegram.pollTimeoutS int 1–300 50 Long-poll timeout against the Telegram API.

Pairing, the throttle, and the asymmetric view between Telegram and the WebUI: Telegram bridge.

Toggles for the built-in tool groups. Only web search and location have UI controls; everything else here is config-only. Full behavior of each tool: Tool reference.

Key Type Default Effect
tools.file.enable bool true Registers the filesystem tools (read_file, write_file, edit_file, apply_patch, list_dir, find_files, grep). Off means the agent cannot touch files at all.
tools.file.exposePackageSource bool true Adds Jenny’s own extracted Python source as an extra read-only root, so the agent can inspect the framework it runs on. Never writable.
Key Type Default Effect
tools.pythonExec.enable bool true Registers python_exec and the exec-session tools.
tools.pythonExec.timeout int ≥ 0 60 Seconds per execution. 0 means no limit.
tools.pythonExec.maxOutputChars int 1000–50000 10000 Output truncation threshold.
tools.pythonExec.allowedModules string[] see below Import allowlist.
tools.pythonExec.blockedModules string[] see below Import denylist.

Defaults — allowed: os, sys, pathlib, json, re, math, datetime, collections, itertools, functools, typing, io, shutil, glob, hashlib, base64, asyncio, csv, platform, time, struct, textwrap, unicodedata, html, xml, dataclasses, enum, uuid. Blocked: subprocess, pty, shlex, multiprocessing, ctypes, socket, signal, termios, tty, grp, pwd, resource, syslog, curses, readline, _thread, fcntl.

httpx and urllib are deliberately absent from the allowlist. A raw HTTP client inside executed code could reach loopback, link-local and RFC1918 targets (SSRF) or read local files through file://, bypassing both the SSRF policy and the workspace policy. Outbound HTTP stays available through the http_get / http_post helpers, which run every URL through the SSRF check. Adding "httpx" back to allowedModules opts out of that protection knowingly. And note what the code itself says: the allowlist is a guardrail, not a sandbox.

Key Type Default Effect
tools.androidWeb.enable bool true Registers web_search and web_fetch. This is the only switch the code checks — the nested search.enable / fetch.enable fields exist in the schema but are not consulted, so they cannot disable one half on their own.
tools.androidWeb.search.searchEngine string "bing" The only supported value; the settings endpoint rejects anything else.
tools.androidWeb.search.maxResults int 5 Results per search. The UI validates 1–10.
tools.androidWeb.search.timeout int 30 Seconds per search. The UI validates 1–120. Also used as the fetch timeout.
tools.androidWeb.fetch.maxChars int 50000 Max characters extracted per page. The UI validates 1000–200000.
Key Type Default Effect
tools.location.enable bool true The user-facing toggle. With it on and the Android permission granted, a last-known position line is injected into the context on every turn — which means it reaches your LLM provider on every turn.
tools.location.telegramTtlS int ≥ 60 3600 How long a location shared through Telegram overrides the GPS reading, for that channel only. Kept in memory, lost on restart.
tools.location.freshTimeoutS int 1–60 15 How long get_location precise=true waits for a fresh GPS fix.

See Location.

Key Type Default Effect
tools.my.enable bool true Registers the my self-inspection tool.
tools.my.allowSet bool false The only restrictive default in the whole tools section. When false, my is read-only and a write attempt returns Error: set is disabled (tools.my.allow_set is false).
tools.introspect.enable bool true Registers get_source — read-only access to Jenny’s own package source.
tools.diagnostics.enable bool true Registers get_recent_logs (in-memory buffer, ~500 lines, cleared on restart). It is the first stop for troubleshooting: ask Jenny to check her own logs.

tools.restrictToWorkspace also appears under tools — it is a mirror, not a setting. See below.

The canonical home for the two policy switches.

Key Type Default Effect
security.restrictToWorkspace bool true Keeps workspace-aware tools inside the workspace (plus skills/, the media directory, and the read-only package source). This is an application-level, fail-closed policy boundary, not an OS sandbox.
security.ssrfWhitelist string[] [] CIDR ranges exempted from the SSRF guard, e.g. ["100.64.0.0/10"] for Tailscale. Keep entries as narrow as possible (192.168.1.50/32).

Two things people get wrong here:

  • The old location still loads, but is not where you edit. A legacy config carrying tools.restrictToWorkspace / tools.ssrfWhitelist and no security block is migrated into security automatically by a validator, and tools.restrictToWorkspace is then kept in sync as a mirror the tool layer reads. Write to security.
  • The SSRF whitelist covers agent tools, not provider calls. It gates web_fetch, download_file, the python_exec HTTP helpers, and media ingestion. Requests to your LLM endpoint do not go through it — a self-hosted model on a private address works without whitelisting anything (see Local models).

Full threat model: Security model.

These govern the WebUI Workspace tab, not the agent’s file tools — the agent is bounded by security.restrictToWorkspace instead.

Key Type Default Effect
workspace.enabled bool true Off makes every /api/workspace/* route answer 503 workspace is disabled — the Workspace tab stops working.
workspace.maxFileSize int 1000000 Max bytes the file viewer will read (1 MB).
workspace.allowWrite bool true Off makes write, mkdir, rename and copy answer 403 workspace writes are disabled.
workspace.allowDelete bool true Off makes delete answer 403 workspace deletes are disabled.

Local versioning of the workspace, plus the key derivation used by encrypted backup export. Snapshots are created by the runtime without involving the LLM.

Key Type Default Effect
snapshots.enabled bool true Master switch for automatic snapshots.
snapshots.scanIntervalMinutes int ≥ 1 5 How often the workspace is scanned for changes.
snapshots.quietMinutes int ≥ 1 10 Quiet period after the last change before a snapshot is taken.
snapshots.dailySafetySnapshot bool true Takes one snapshot a day even without qualifying changes.
snapshots.retentionRecent int ≥ 1 20 The most recent N snapshots are always protected from pruning, including from the age horizon.
snapshots.retentionThinAfterDays int ≥ 1 30 Beyond this age, history is thinned to roughly one snapshot per day.
snapshots.retentionMaxAgeDays int ≥ 0 0 Age horizon in days; 0 means keep forever. The Settings selector maps to 7 / 30 / 365 / 0. Changing retention prunes immediately.
snapshots.pbkdf2Iterations int 100000–10000000 600000 PBKDF2 iterations for the exported .jbk backup key. The ceiling mirrors the container format’s own limit.
snapshots.excludeGlobs string[] see below Paths never captured.

Default excludes: ui/**, logs/**, .jenny/logs/**, .jenny/snapshots/**, .jenny/backup_staging/**, **/__pycache__/**, *.tmp, *.tmp.*. See Backup and restore.

Key Type Default Effect
apps.enabled bool true Enables the Jenny Apps runtime and the dynamic <slug>_<action> tools it registers.
apps.httpTimeoutS float 1–120 20.0 Timeout for an app’s outbound HTTP proxy calls.
apps.maxCollectionBytes int 5000000 Per-collection storage ceiling (5 MB). Writes past it fail with 413.

See Mini-apps.

Key Type Default Effect
wiki.enabled bool true Off makes every wiki route answer 503.
wiki.wikisDir string "wikis" Directory holding the wikis, relative to the workspace.
wiki.defaultWiki string "main" Wiki opened when none is specified.
wiki.extensions string[] ["fenced_code", "tables", "toc", "wikilinks", "mermaid"] Python-Markdown extensions used to render wiki pages. Mermaid renders here and only here — not in chat.

See Wiki.

Named bundles of model settings you can switch between at runtime with /model <name>. modelPresets is a top-level object; its keys are your own preset names.

{
"modelPresets": {
"fast": {
"label": "Fast",
"provider": "openai",
"model": "gpt-4.1-mini",
"maxTokens": 4096,
"contextWindowTokens": 65536,
"temperature": 0.2,
"reasoningEffort": "low"
},
"deep": {
"label": "Deep",
"provider": "anthropic",
"model": "claude-opus-4-5",
"maxTokens": 8192,
"reasoningEffort": "adaptive"
}
},
"agents": { "defaults": { "modelPreset": "fast" } }
}
Field Type Default Effect
label string | null null Display name.
provider string | null null Name of a configured provider entry.
model string | null null Model ID. Falls back to the current model when null.
maxTokens int | null null Overrides output tokens while the preset is active.
contextWindowTokens int | null null Overrides the context budget. Falls back to the current value when null.
temperature float | null null Overrides the sampling temperature.
reasoningEffort string | null null Per-preset reasoning effort — including none and adaptive. This is the tidiest way to keep several thinking levels one command apart.

How switching behaves:

  • /model with no argument prints the current model, the active preset ((none) when unset), and the available preset names.
  • /model <name> applies the preset for future turns — model, context window, and generation settings on the active provider. An unknown name returns Could not switch model preset: … followed by the available names.
  • A preset’s provider field selects request routing where the active provider supports it; provider objects are not swapped mid-run, so a preset pointing at a genuinely different backend needs an app restart (or a provider change from Settings, which does hot-reload).
  • Runtime switches are not written back to config.json. agents.defaults.modelPreset decides what you start with after a restart.

Jenny · code underAGPL-3.0 · name and mascot undertrademark

No cookies, no trackers, no analytics. This page loads nothing from a third party.