type: operations title: Configuration and data files description: How src/config.rs loads config.toml, the system prompt from SOUL.md and the optional image description request from VISION.md, how src/config_watch.rs reloads them live every 2 seconds, the supported keys and defaults (reply_delay_seconds, catch_up_seconds, ai_retries, web_port, transcribe_key/url/model/language), the template files, where the data directory and databases live, the build version and release profile, and operational and security caveats. tags: [configuration, toml, data-directory, sqlite, security, soul-md, live-reload, transcription] sources:
ea70eb6c04
resource: repo://.gitignore651d1fb6c9
resource: repo://Cargo.toml0d5b861992
resource: repo://config.toml.global.template1b05507d70
resource: repo://src/ai.rs9fec9d1af4
resource: repo://src/app.rsd6b2055bbb
resource: repo://src/config_watch.rs2a737474d8
resource: repo://src/config.rsb55a21a31e
resource: repo://src/main.rs
generated: { by: "opencode", at: "2026-10-24T16:39:50.206Z" }
---All runtime settings come from files in the data directory, loaded by
Config::load (src/config.rs:325-335): config.toml for the settings,
SOUL.md for the system prompt and the optional VISION.md for the image
description request. No environment variable is ever read
(src/config.rs:1-29). The files are loaded at startup and reloaded
automatically when they change (see Live reload); only
web_port needs a restart.
The data directory is fixed: <platform config dir>/wh, resolved with
dirs::config_dir() (src/config.rs:206-210). On Windows this is
%APPDATA%\wh, on Linux ~/.config/wh (file names are case-sensitive
there). It cannot be changed from the config file. At startup
ensure_data_dir creates it if missing (src/config.rs:372-375).
| File | Purpose | Defined by |
|---|---|---|
config.toml |
Optional settings | CONFIG_FILE, src/config.rs:64 |
SOUL.md |
System prompt: the responder's identity and rules; required for the AI | SOUL_FILE, src/config.rs:66 |
VISION.md |
Optional request sent with each image to obtain its description | VISION_FILE, src/config.rs:68 |
session.db |
WhatsApp session store used by whatsapp-rust (WhatsApp client) |
session_db_path, src/config.rs:389-391 |
history.db |
Local messages, chats, contacts, per-chat autoreply flags, the global autoreply setting and image descriptions (History store) | history_db_path, src/config.rs:397-399 |
ai_requests.log |
Optional AI request log, written only when ai_log = true (AI client) |
ai_log_path, src/config.rs:405-407 |
The two databases are separate by design: session.db belongs to the
WhatsApp library (linked-device session and keys, schema owned by
whatsapp-rust), while history.db belongs to the app.
config.toml| Key | Purpose | Default when missing or blank |
|---|---|---|
openai_api_key |
Bearer token for the AI endpoint (trimmed) | none — the AI client cannot be built |
openai_base_url |
Base URL of the OpenAI-compatible endpoint (trimmed, trailing / removed) |
https://ollama.com/v1 |
openai_model |
Model for text replies (trimmed) | deepseek-v4.1-flash |
openai_vision_model |
Model that describes images (trimmed) | none — image understanding disabled |
ai_log |
Boolean; when true every request sent to the AI model is appended to ai_requests.log |
false |
reply_delay_seconds |
Seconds the autoreply waits after the first unanswered message of a chat; messages received meanwhile are answered in the same reply; 0 answers at once |
30 |
catch_up_seconds |
After a restart, the last unanswered message of each private chat is answered once if it is at most this old; 0 disables the catch-up |
7200 |
ai_retries |
How many times a failed AI request is retried, waiting 2 s, 4 s, …; HTTP 4xx other than 429 is never retried; 0 disables retrying |
2 |
web_port |
TCP port of the web control panel on 0.0.0.0, no authentication; valid range 1–65535 |
unset — panel disabled |
transcribe_key |
API key of a cloud, OpenAI-compatible audio/transcriptions endpoint (trimmed); audio transcription is enabled only when set |
unset — voice notes stay untranscribed |
transcribe_url |
URL of the transcription endpoint (trimmed, trailing / removed) |
https://api.groq.com/openai/v1/audio/transcriptions |
transcribe_model |
Model of the transcription endpoint (trimmed) | whisper-large-v3-turbo |
transcribe_language |
Language passed to the transcriber (trimmed); auto detects the language |
auto |
Mapping and defaults: Config::from_file (src/config.rs:342-366) and the
constants at src/config.rs:38-53. reply_delay_seconds,
catch_up_seconds and ai_retries are unsigned integers in the file
(Option<u32>, src/config.rs:143-148), so a negative value is rejected as
an invalid value type. web_port is Option<u16> (src/config.rs:149-151),
so negatives and values above 65535 fail TOML parsing, and
resolve_web_port (src/config.rs:312-317) additionally rejects 0. Their
use is described in AI autoreply,
AI client and
Web control panel.
Unknown keys are ignored. In particular the keys of older versions are no
longer read: system_prompt (move the prompt to SOUL.md),
cooldown_seconds / group_cooldown_seconds (replaced by
reply_delay_seconds) and transcribe_command (removed local transcription
command, replaced by the cloud endpoint keys); a leftover value is silently
ignored, not an error.
config.toml.global.template in the repository root lists every key with
its default and a comment (with web_port and the transcribe_* keys
commented out) and points to SOUL.md for the prompt; copy it to the data
directory as config.toml and fill in openai_api_key.
SOUL.md and VISION.mdread_prompt_file (src/config.rs:259-265) reads both files:
None;cannot read prompt file <path>), which aborts startup.SOUL.md becomes Config::system_prompt. The binary embeds no prompt text:
without SOUL.md (or with a blank one) the AI is not configured, every
autoreply path stops and the status line says
AI non configurata: imposta openai_api_key in config.toml e il prompt in SOUL.md
(see Application state). soul.md.template
in the repository root is a minimal starting point. The app appends only
factual context to the prompt (chat kind and name, who writes the user and
assistant messages, see AI autoreply);
behaviour — including when to call the no_reply tool and stay silent —
belongs in SOUL.md.
VISION.md becomes Config::image_description_prompt; when it is missing or
blank the AI client uses its built-in Italian
description request. The vision model receives only this request and the
image, never the conversation, so tone and reply style belong in SOUL.md.
The file was previously named vision.md: on Linux, where names are
case-sensitive, an old lowercase file is no longer read and must be renamed.
config.tomlread_config_file, src/config.rs:242-252).cannot read config file <path>.invalid config file <path> with the
cause invalid TOML syntax or value type at line L, column C; the error
never echoes file content (parse_config_file, src/config.rs:227-236).
This also covers negative or non-integer reply_delay_seconds,
catch_up_seconds and ai_retries, a non-boolean ai_log and an
out-of-range web_port.openai_base_url consisting only of slashes is rejected with
openai_base_url in config.toml must not be empty or only slashes
(src/config.rs:287-294); a transcribe_url that is blank or only
slashes instead falls back to the default (src/config.rs:300-306).web_port = 0 is rejected with
web_port in config.toml must be between 1 and 65535.Startup aborts with cannot load configuration on any of these errors
(Runtime); during a live reload
the same errors only keep the current configuration.
main starts config_watch::spawn(config.source_files(), app_tx).
Config::source_files (src/config.rs:381-383) lists config.toml,
SOUL.md and VISION.md inside the data directory. The watcher
(src/config_watch.rs:24-40) polls their modification times every 2 seconds
(POLL_INTERVAL_SECONDS) instead of using file notifications, so it needs
no extra dependency and behaves the same on Windows and Linux. A file whose
metadata cannot be read counts as None, so creating or deleting a file is
also a change. On any change it runs Config::load again and sends
AppEvent::ConfigReloaded with the new Config or the error text; the task
ends when the application receiver is dropped.
App then:
Config, rebuilds the AI client (replies already
in flight keep the old one) and shows Configurazione ricaricata, or
AI non configurata: ... / AI non disponibile: ... when the new files
leave the AI unusable;Configurazione non ricaricata: <error>.New values of the AI keys, SOUL.md, VISION.md, ai_log,
reply_delay_seconds, catch_up_seconds, ai_retries and the
transcribe_* keys therefore apply to the next replies without restarting.
Note that audios arriving while transcription was disabled are not
transcribed later; only new audios are. web_port does not apply: the web
panel is bound once at startup. Saving a file in several steps can trigger
more than one reload; each one reloads the full set of files.
wh --version (or -V) prints wh <version> (<short git commit>) and
exits without reading any file; the same text is shown at the bottom right
of the main screen. The commit hash is embedded at build time by build.rs
and is unknown when built without git (see
Runtime and event loop).
The release profile only strips symbols (Cargo.toml:31-33); LTO and a
single codegen unit are no longer used, which keeps release rebuilds fast.
TLS for the AI client uses ring only: reqwest is built with
rustls-no-provider and the ring provider is installed when the
AI client is built, so the large aws-lc-sys
C build is not needed, which matters for the Linux musl cross-compile with
cargo-zigbuild described in README.md.
Config's Debug shows *** or <unset>
for both openai_api_key and transcribe_key
(src/config.rs:174-200), and the raw FileConfig has no Debug impl at
all (src/config.rs:129-160). The AI client redacts its key too, and the
transcription client never logs or echoes its key.ai_requests.log never contains the API key and replaces image data
with a size placeholder, but it does store the full request (system
prompt and chat context) and the reply in clear text, one record per
attempt including retries, and it is only appended to, never rotated.
Enable ai_log only for diagnosis and delete the file afterwards.transcribe_key unset if
that is not acceptable. Audio bytes are kept in memory only and never
written to the data directory; only the transcribed text ends up in
history.db inside the voice-note message.web_port exposes an unauthenticated HTTP panel on every network
interface: anyone who can reach the port can see chat names, toggle
autoreplies, quit the bot, unlink the WhatsApp device and, while pairing,
scan the QR code. Set it only on a trusted network or
firewall the port. A port already in use aborts startup with
cannot start web server on port <port>.config.toml, SOUL.md and VISION.md in the data directory,
outside the repository; the two prompt files are personal. .gitignore
ignores a root-level /config.toml, .env, any SOUL.md, any
VISION.md and *.log files (.gitignore:4-19), as well as SQLite
journal/WAL/SHM files; note that the *.db pattern is commented out
(.gitignore:7), so databases copied into the repo would not be ignored.session.db holds the linked-device credentials. Copying it to another
machine reuses the linked session, but never run both copies at the same
time. To pair again without restarting, press F8 twice or use the web
panel's "Nuovo QR": the device is logged out and session.db is deleted
or emptied in place (see
WhatsApp relink and pairing).
history.db can be deleted to reset local history, image descriptions and
the per-chat and global autoreply flags; it is recreated on start.history.db.src/config.rs:413 onward covers defaults, full files, the ignored legacy
keys (system_prompt, cooldown_seconds / group_cooldown_seconds and
transcribe_command), blank values, trimming, base URL normalisation and
errors, Debug masking, vision model, DB and AI log paths, ai_log,
reply_delay_seconds (default, custom values including 0, negatives
rejected), catch-up and ai_retries defaults and negatives, web_port
(unset, valid, 0 and out-of-range rejected), the transcription keys
(unset disables with defaults, parsed and trimmed, blank falls back),
unknown keys, BOM/CRLF, non-leaking parse errors, missing, existing, invalid
and unreadable files, and SOUL.md reading (missing, blank, kept as written
without BOM, non-UTF-8 error naming the path). src/config_watch.rs checks
that modification-time snapshots change when a watched file is created and
deleted. See Unit tests.