Repository navigation
Data and Retention
Keryx follows the XDG directory layout on both Linux and macOS. It honors each
XDG_*_HOME you set, but ignores one that's empty or a relative path. To see where each
directory is on your machine, run keryx config path.
| Directory | What's in it |
|---|---|
~/.config/keryx (KERYX_HOME) |
config.toml, secrets.toml, the PIN, and the Google client file |
~/.local/share/keryx (DATA_DIR) |
tasks and their reports, call transcripts, the memory, sign-in tokens, and the plugins and your own tools (tools/) |
~/.local/state/keryx (STATE_DIR) |
logs, the restart record, and the approval bridge's socket |
~/.cache/keryx (CACHE_DIR) |
files that can be downloaded again |
Only you can read these directories. Directories use mode 0700 and files use
0600, and keryx doctor checks both. ~/.config/keryx holds secrets.toml and the
PIN, so keep it out of any dotfiles repository. Data isn't encrypted at rest, and by
default nothing is deleted automatically.
| Path | What's in it |
|---|---|
KERYX_HOME/config.toml, secrets.toml
|
your settings, and every key and token |
KERYX_HOME/pin |
the phone PIN, written once — see Security Model |
KERYX_HOME/google_client_secret.json |
your Google OAuth client, if you connected Google |
DATA_DIR/calls/<session_id>.log |
the full transcript of a call: every line you said and every line the assistant said. A spoken PIN is written as [PIN]. The first line says whether the call began authorized, and a --- authorized line marks where the PIN was accepted during the call. |
DATA_DIR/tasks.db |
every task: what you asked for, its status, and the spoken summary that came back |
DATA_DIR/tasks/<id>.log, .md
|
a task's progress, tool by tool, and the written report it produced |
DATA_DIR/workspace/ |
where a task with no project runs when PROJECTS_ROOT doesn't exist, and whatever it leaves there |
DATA_DIR/memory.md |
what Keryx carries between calls. It starts from keryx memory seed or keryx setup, if you wrote a first draft, and is rewritten after each call that gave the PIN. |
DATA_DIR/projects/<name>.md |
project summaries that keryx setup drafted and you accepted, for repositories with no .keryx-brief.md of their own |
DATA_DIR/tools/ |
your own voice tools (see Custom Tools), and the plugins you turned on. Each plugin is a one-line <name>.py and its settings in <name>.toml. After keryx plugins remove, the settings file is renamed <name>.toml.off. No secret is kept here: a plugin's token or admin key stays in secrets.toml, and the Gmail sign-in has its own file. |
DATA_DIR/gmail_token.json, google/
|
the read-only Gmail sign-in, and the agents' Google sign-in |
DATA_DIR/codex/ |
Codex's own login, when it signs in with CODEX_API_KEY
|
DATA_DIR/pin-failures.json |
wrong PINs across calls, and the lock they set — see Security Model |
DATA_DIR/report_secret |
the key that signs /reports/{id} links, if REPORT_SECRET isn't set |
STATE_DIR/logs/*.log |
the server log, the service manager's stdout/stderr, and the restart watchdog's output |
STATE_DIR/restart.json, running-version
|
restart bookkeeping |
STATE_DIR/approvals.sock, approvals/
|
the approval bridge's socket, its pending marker, its kill switch, and its audit log |
A PIN is never written down. Keyed digits aren't written anywhere: not to the log, not
to the transcript, and not into the model's context. The realtime model hears a PIN you
say, because that's how it gets checked. But every transcript line is redacted as it's
written, and no log line carries the PIN. Redaction works whether the transcription came
out as digits (1 2 3 4 5 6, 123-456) or as words (one two three…, with oh or
zero for 0).
-
OpenAI Realtime API: call audio, the conversation, and the system prompt. The system
prompt is sent again on every call, and carries
memory.mdand every project's brief. Once a PIN exists, they're sent from the greeting. WithBRIEFING_BEFORE_PIN=false, they're sent only after the PIN. - OpenAI Responses API: the small factual questions that the voice model answers itself.
- Anthropic, through the Claude Agent SDK: the task text, and whatever the task reads. When a task runs on Codex, these go to OpenAI instead.
-
Twilio: phone audio and caller ID. Texts go through Twilio too, if you turned them on
with
SMS_ENABLED=true. -
Google: Gmail and Calendar work, through the Claude CLI's own connectors, or through
workspace-mcpif you turned it on.
The memory and the briefs are bounded. A call reads 4,000 characters of memory. Each brief
is capped at 1,500 characters, and all briefs together at 6,000. A brief that would go
past the total is left out entirely, and the log names it. keryx memory seed tells you
how many characters they add up to on this machine. Don't put anything in either that you
wouldn't send to OpenAI.
The plugins send nothing until you turn them on:
-
send_to_slacksends to Slack, but only when you ask. So does a task's use of the Slack MCP server named bymcp_serverinsend_to_slack.toml. -
check_emailreads Gmail read-only and summarizes it with theclaudeCLI, so what it reads goes to Anthropic too. -
check_billingreads the provider's billing API. -
cluster_statsruns read-only Slurm commands on the hosts you named incluster_stats.toml. It uses only an ssh ControlMaster that you already have open, or a guard script of your own, and never opens a connection itself.
A tool of your own reaches whatever its code reaches. Keryx doesn't send any telemetry or analytics of its own.
Nothing is deleted by default. keryx.log rotates, but transcripts, task rows, task logs,
and task reports stay until you set up retention or delete them. Clear this data before
you pass the machine on to someone else.
Two settings turn retention on, independently. 0, the default, means keep everything:
| Setting | What it prunes | Default |
|---|---|---|
TRANSCRIPT_RETENTION_DAYS |
calls/*.log older than this |
0 — never |
TASK_RETENTION_DAYS |
finished task rows, and their .log and .md, older than this |
0 — never |
Set them with keryx config set, for example keryx config set TRANSCRIPT_RETENTION_DAYS 30. Pruning runs once when keryx serve starts, so a new value
takes effect on the next restart. To prune now instead:
uv run keryx forget --older-than 30 # asks first
uv run keryx forget --yes # everything eligible, no window
uv run keryx forget --transcripts-only --older-than 7--older-than 0 really does mean everything, and none of it can be undone. You can't
combine --transcripts-only and --tasks-only.
A finished task stays until Keryx has told you its result. Keryx records when it told you,
and a task it hasn't told you about is never pruned. keryx forget reports how many tasks
it kept for this reason. Keryx's own housekeeping tasks and canceled tasks follow the
normal schedule.
Nothing prunes DATA_DIR/tools/. A plugin's settings stay until you delete the files, and
keryx plugins remove keeps them aside for the next time you turn the plugin on.
memory.md works differently. keryx memory seed can write the first one, and a subagent
rewrites it after every authorized call. When that subagent finishes, Keryx trims the file
to a size limit, so it doesn't grow without limit, even though nothing prunes it.
uv run keryx memory prints it. It's plain Markdown that you can edit by hand, and the
next update keeps your edits.
For how secrets are stored, see Where secrets live in SECURITY.md.
User documentation for Keryx.
For the threat model, see SECURITY.md;
for settings, the coding agents, and the tools, see docs/.
Apache-2.0, Copyright 2026 William Koch.
Setting up
Running it
Security and data
Tools and advanced features