Configuration keys¶
Every key keryx reads, with its default and what happens when it is wrong. Keys are written here in dotted form; in YAML they nest. Where the files live and which layer wins is on the configuration page.
A key not listed here is not read. In particular, studio.port appears in some older
notes and is not a configuration key — the studio's port comes from --port only.
workspace.* — where reel workspaces live¶
| Key | Type | Default | Meaning |
|---|---|---|---|
workspace.root |
string | reels |
Directory, relative to the project root, holding one subdirectory per reel workspace. |
keryx reel new <slug> creates <workspace.root>/<slug>, and keryx reel list
enumerates that directory.
workspace.root is ignored by every command that takes --workspace
Passing --workspace <slug> binds the flag into the highest-precedence
configuration layer under the key workspace, which replaces the workspace
mapping the files supplied. workspace.root then resolves to empty and the command
falls back to the built-in reels.
So on a project that sets workspace.root, reel new and reel list use the
configured directory, while reel build, reel plan, reel lock, reel prune,
voice takes and every other --workspace command look under reels/ and report
workspace not found. reel prune <slug> given as a positional argument is
unaffected.
Until this is fixed, leave workspace.root unset and keep reel workspaces in
reels/.
reel.* — frame shape¶
| Key | Type | Default | Meaning |
|---|---|---|---|
reel.aspect |
string "W:H" |
9:16 |
Default frame shape for a reel. The short side is always 1080 px; the long side scales and rounds to an even number. |
A reel workspace can override this per reel with aspect in its workspace.yaml, and
reel build --aspect overrides both. An aspect more elongated than 3:1 in either
direction is rejected: aspect "5:1" is too elongated (beyond 3:1).
render.* — which engine does the work¶
| Key | Type | Default | Meaning |
|---|---|---|---|
render.driver |
string | auto |
auto (native driver where published for this host, wasm otherwise), off, on (fail rather than fall back), or a path to a driver binary. |
Applies to the afmpeg backend, which can run either the portable WASM module or a
native driver that is dramatically faster — seconds against roughly eight and a half
minutes on a three-card reel, per render.
It sits here rather than under providers.render.* on purpose. providers.render as
a scalar naming the adapter and providers.render.driver as a map key cannot coexist
in YAML, so a key under providers.render.* would be unreachable for precisely the
people who selected afmpeg in config — everyone who might want to set it. A
top-level render.* has no such collision:
KERYX_FFMPEG_WASI_DRIVER overrides it for a single run.
When auto falls back, keryx says so — an INFO line on the CLI, and a note on the
studio's render panel while the render runs. A render that is merely slow is
otherwise indistinguishable from one that has hung.
providers.* — selecting a backend¶
Each generative capability sits behind an interface, and the adapter is chosen by a scalar key. An empty or unset value selects the default; an unregistered name is an error listing what is available.
| Key | Default | Adapters actually registered |
|---|---|---|
providers.image |
gemini |
gemini |
providers.voice |
elevenlabs |
elevenlabs |
providers.music |
elevenlabs |
elevenlabs |
providers.render |
ffmpeg |
ffmpeg, afmpeg |
providers.video |
gemini |
none — video generation is not implemented, so resolving this capability always fails |
Chat is selected differently, by a nested key — see
providers.chat.* below.
The scalar and map forms of a providers key collide¶
providers.render is a scalar (the adapter name). providers.render.module is a key
inside a map. One YAML key cannot be both, and neither can two layers: the
higher-precedence layer's shape wins outright and the other form reads as empty.
# Rejected by the YAML parser — "mapping key render already defined".
providers:
render: afmpeg
render:
module: /opt/ffmpeg-wasi.wasm
Splitting them across ~/.keryx/config.yaml and the project .keryx.yaml does not
help either; whichever layer is higher supplies the shape and the other contributes
nothing.
The same collision applies to providers.image versus providers.image.price_per_image
and providers.image.currency, and to providers.voice versus
providers.voice.price_per_1k_chars. Choosing a non-default adapter and tuning that
adapter's per-unit price cannot both be expressed.
providers.render.* — the afmpeg wasm module¶
Read only when providers.render is not set as a scalar, for the reason above.
| Key | Type | Default | Meaning |
|---|---|---|---|
providers.render.module |
string | the pinned certified release | A host path to a .wasm, or an https:// URL. A .gz URL is decompressed on download. |
providers.render.module_sha256 |
hex string | none | SHA-256 of the decompressed .wasm, verified after download. Applies only to a module URL you set yourself. |
providers.render.cache_dir |
path | OS user-cache directory | Where a downloaded module is cached. Applies to the pinned release too. |
KERYX_FFMPEG_WASI is the env fallback for providers.render.module, and — because of
the collision — it is the only way to override the module while also selecting
providers.render: afmpeg.
With nothing set, the afmpeg backend downloads a pinned ffmpeg-wasi release (the
gpl variant), caches it, and reuses it. You do not need to fetch a module by
hand.
That default goes through the project's certified release path: the module is fetched with its checksums, detached signature and provenance, and all three are verified against signing keys embedded in the keryx binary before the module is compiled. Verification cannot be skipped on this path.
Setting providers.render.module yourself is the bring-your-own path instead — your
build, uncertified, with module_sha256 as the integrity check you supply. It is not
routed through the release verification, because a module you built was never issued
under that provenance.
providers.chat.* — the LLM for drafting and copy¶
| Key | Type | Default | Meaning |
|---|---|---|---|
providers.chat.provider |
string | none — see below | claude, claude-local, gemini, openai or openai-compatible. |
providers.chat.model |
string | the provider's own default | Model id passed to the chat client. |
There is no hardwired chat provider. When providers.chat.provider is unset, keryx
infers it from exactly one API key being present in the environment:
- one of
ANTHROPIC_API_KEY,GEMINI_API_KEYorOPENAI_API_KEYpresent → that provider is used; - none present →
no chat provider configured — set providers.chat.provider (claude|gemini|openai) + the matching API key; - more than one present →
multiple chat API keys present (…) — set providers.chat.provider to choose.
That last case is the one that catches people mixing vendors. Generating images with
Gemini needs GEMINI_API_KEY; if you also want chat from OpenAI you will have two keys
in the environment, and inference refuses rather than guessing. Set
providers.chat.provider: openai explicitly and both work — image and chat provider
selection are independent keys, and using different vendors for them is supported.
providers.gemini.* and providers.elevenlabs.* — adapter settings¶
| Key | Type | Default | Meaning |
|---|---|---|---|
providers.gemini.model |
string | the adapter's Imagen → Gemini fallback chain | Forces an image model id. Routed by prefix: imagen* uses the Imagen :predict endpoint, anything else :generateContent. |
providers.gemini.base_url |
URL | Google's endpoint | Override the API base. |
providers.elevenlabs.base_url |
URL | ElevenLabs' endpoint | Override the API base. |
cover --model and portrait --model override providers.gemini.model for one run.
providers.*.price_* — cost estimates¶
Prices are cues shown before you spend, not invoices. They are not vendor rates and are deliberately round.
| Key | Type | Default | Priced per |
|---|---|---|---|
providers.image.price_per_image |
float | 0.03 |
image |
providers.voice.price_per_1k_chars |
float | 0.30 |
1000 characters of narration |
providers.music.price_per_second |
float | 0.008 |
second of bed |
providers.chat.price_per_compose |
float | 0.01 |
social composition |
providers.image.currency |
string | USD |
the ISO code shown against every unit, not just images |
spend.* — the runaway-batch guard¶
| Key | Type | Default | Meaning |
|---|---|---|---|
spend.confirm_above.amount |
float | 10.0 |
Currency cap per request for flat-priced units (image, music, social). |
spend.confirm_above.characters |
float | 50000 |
Character cap per request for voice. |
Voice is measured on the character axis and everything else on the currency axis. Zero on an axis disables that axis.
themes.* — the style catalog¶
| Key | Type | Default | Meaning |
|---|---|---|---|
themes.default |
string | none | The default theme keyword, used by every target when --theme is omitted. |
themes.<keyword> |
map | — | A theme definition. |
default and defaults are reserved: themes.default holds the default
keyword, so no theme may claim either name.
The pre-0047 themes.<type>.<keyword> nesting and themes.defaults.<type> are
no longer read. A catalog still in that shape is refused with the command to
run — see upgrading from a typed catalog.
Themes are not seeded. On a fresh install there is no catalog, and the first
generation asks for one:
no theme configured — add one with keryx theme add <keyword> (the first theme added becomes the default).
The field-by-field shape of a theme — palette, card treatment, scrim, music gain, voice clone settings — is on the themes page.
voices.* — the speaker registry¶
voices.<name> defines a named narration voice, referenced from a storyboard card's
voice.speaker for multi-author reels. The fields are the same as a reel theme's
voice block: id, stability, similarity, style, speed, speaker_boost,
model, pronunciation.
Naming a speaker that is not registered fails with
voice "<name>" not found (configure it under voices.<name>).
keryx init seeds a voice called matt carrying a real ElevenLabs voice id belonging
to the project's author. It is an example, not a default you want; delete it or replace
it with your own.
avatars.* — the actor registry¶
avatars.<name> defines a recurring character that can be composited into card
illustrations, selected per reel through workspace.yaml's avatars list and per card
through the storyboard's avatars field.
| Field | Meaning |
|---|---|
ref |
Character reference image, project-relative. |
likeness |
Theme-independent description of the person. |
style |
The avatar's own base art style, used when not restyling to a theme. |
palette |
The avatar's own palette, folded into style use. |
postamble |
Overrides the built-in prompt postamble (expression lock, no-text rule, 9:16 framing). |
Naming an unregistered avatar fails with
avatar not found: "<name>" (configure it under avatars.<name>).
As with voices, keryx init seeds an example avatar called matt describing the
project's author, whose ref path will not exist in your project.
log.level — logging verbosity¶
| Key | Type | Default | Meaning |
|---|---|---|---|
log.level |
string | info |
Framework log level. --debug overrides it for one run and survives a config reload. |
studio.* — the local web studio¶
| Key | Type | Default | Meaning |
|---|---|---|---|
studio.take_count |
int | 4 |
Candidate takes a studio generation produces. Clamped to 1–12; a value below 1 becomes 1, above 12 becomes 12. |
studio.takes_ttl |
duration | 3m |
Grace window before an unpicked in-memory candidate take is reclaimed. Accepts Go duration strings, e.g. 5m. |
studio.takes_ttl only bites on an in-memory project (a RAM worktree of a remote
git project). A local-disk project never prunes takes; its takes are ordinary files the
workspace lifecycle owns, cleared by reel prune. The
sweeper runs every 30 seconds and skips anything that is the picked slot, so
"generate a second round, then go back and pick from the first" works within the
window.
The studio's bind port and host come from --port and --host; there is no config key
for either.
git.* — the studio's git behaviour¶
| Key | Type | Default | Meaning |
|---|---|---|---|
git.commit_on_save |
bool | true |
The studio commits each saved change to the project repo. |
git.auto_push |
bool | false |
Exposed in the studio Settings panel as "push on save". Nothing reads it — setting it has no effect. |
storage.* — the media object store¶
Unset storage.provider means local-only, which is not an error: media stays in the
workspace and the sync commands report the store as unconfigured.
| Key | Type | Default | Meaning |
|---|---|---|---|
storage.provider |
string | unset | s3, gcs, azure, file or mem. |
storage.prefix |
string | empty | Project namespace inside a shared bucket. A blank prefix drops that path segment, giving keys of the form reels/<slug>/<relpath>. |
Credentials are never read from keryx config. Each backend resolves them through its own chain — the AWS default chain, Google ADC, the Azure default credential.
| Key | Applies to | Required | Meaning |
|---|---|---|---|
storage.s3.bucket |
s3 |
yes | Bucket name. Missing: storage.s3.bucket is not set. |
storage.s3.region |
s3 |
no | Overrides the AWS chain's region. |
storage.s3.endpoint |
s3 |
no | Endpoint for an S3-compatible store; setting it also switches to path-style addressing. |
storage.s3.profile |
s3 |
no | A named AWS profile. A profile name, not a credential. |
storage.gcs.bucket |
gcs |
yes | Bucket name. |
storage.azure.container |
azure |
yes | Container name. |
storage.azure.account |
azure |
no | Storage account name. |
storage.file.dir |
file |
yes | Local directory used as the object store. |
mem keeps objects in process memory and is there for tests; nothing survives the
process.
platforms.* — per-platform posting¶
platforms.<name>.enabled (bool, default false) gates a platform for
post and auth refresh.
<name> is instagram, youtube, tiktok or linkedin.
Credentials resolve environment variable → OS keychain → config key, in that order. The config fallback is plaintext; prefer the environment or the keychain. The full credential set-up per platform is in the how-to guides (Instagram, YouTube, TikTok, LinkedIn).
Non-secret platform settings:
| Key | Default | Meaning |
|---|---|---|
platforms.instagram.api_base |
https://graph.instagram.com/v21.0 |
API base URL. |
platforms.instagram.app_id, .user_id, .redirect_uri |
unset | Non-secret identifiers used by the OAuth flow. |
platforms.youtube.privacy |
private |
Upload privacy status. |
platforms.youtube.category_id |
22 (People & Blogs) |
YouTube category id. |
platforms.youtube.client_id, .channel_id |
unset | Non-secret identifiers. |
platforms.tiktok.api_base |
https://open.tiktokapis.com/v2 |
API base URL. |
platforms.tiktok.privacy |
SELF_ONLY |
Post visibility. Stays self-only until TikTok audits the app. |
platforms.tiktok.client_key, .open_id |
unset | Non-secret identifiers. |
platforms.linkedin.api_base |
https://api.linkedin.com |
API base URL. |
platforms.linkedin.api_version |
202606 |
LinkedIn versioned-API header. |
platforms.linkedin.visibility |
PUBLIC |
PUBLIC or CONNECTIONS. LinkedIn has no "only me". |
platforms.linkedin.client_id, .author_urn |
unset | Non-secret identifiers. |
platforms.<name>.constraints.* — overriding the composition limits¶
Each platform ships built-in norms used to steer composition and to gate approval. Every field can be overridden per platform, so a limit can be corrected without waiting for a release.
| Key | YouTube | TikTok | ||
|---|---|---|---|---|
constraints.text_cap |
2200 | 5000 | 2200 | 3000 |
constraints.title_cap |
0 (no title field) | 100 | 0 | 0 |
constraints.links_clickable |
false |
true |
false |
true |
constraints.hashtag_hint |
several–~10 (cap 30) | a few, in the description | a few, trend-led; front-load text | sparing (1–3); front-load ~140 |
Exceeding text_cap or title_cap is a hard violation: keryx approve refuses the
platform. A title on a platform with title_cap: 0, or a link on a platform with
links_clickable: false, is advisory — you get
instagram links are not clickable — use "link in bio" and posting still proceeds.
auth.writeback.* — where refreshed tokens are written¶
| Key | Type | Default | Meaning |
|---|---|---|---|
auth.writeback.backend |
string | local |
local writes rotated credentials back to ~/.keryx/accounts.yaml; gitlab writes them to a project's CI/CD variables. |
auth.writeback.gitlab.project |
string | CI_PROJECT_ID |
Project id or path to write variables into. |
auth.writeback.gitlab.api_url |
URL | CI_API_V4_URL, else https://gitlab.com/api/v4 |
GitLab API base. |
The gitlab backend needs GITLAB_TOKEN (api-scoped) in the environment and refuses
without it.
auth.alerts.* — expiry alerting¶
| Key | Type | Default | Meaning |
|---|---|---|---|
auth.alerts.backend |
string | none |
none, webhook or email. |
auth.alerts.webhook.url |
URL | unset | Webhook target. ALERT_WEBHOOK_URL takes precedence. |
auth.alerts.email.host |
string | unset | SMTP host. Required for email. |
auth.alerts.email.port |
int | 587 |
SMTP port. |
auth.alerts.email.from |
string | unset | Envelope sender. Required for email. |
auth.alerts.email.to |
list | unset | Recipients. Required for email. |
auth.alerts.email.username |
string | unset | SMTP user. ALERT_SMTP_USERNAME takes precedence. |
auth.alerts.email.password |
string | unset | SMTP password. ALERT_SMTP_PASSWORD takes precedence. |
webhook without a URL fails with
auth.alerts.backend=webhook but no URL — set ALERT_WEBHOOK_URL or auth.alerts.webhook.url;
email missing any of host, from or to fails with
auth.alerts.backend=email needs auth.alerts.email.{host,from,to}.
tls.source — the studio's certificate¶
| Key | Type | Default | Meaning |
|---|---|---|---|
tls.source |
string | localca |
localca provisions a browser-trusted per-machine certificate; anything else uses a self-signed one. |
An unknown value is not an error — it degrades to self-signed, and HTTPS is still
served so the Secure cookie holds. localca also falls back to self-signed when the
CA cannot be provisioned (no elevation, no certutil).
site.base_url — the canonical post URL¶
| Key | Type | Default | Meaning |
|---|---|---|---|
site.base_url |
URL | unset | Base URL of the site the reel points at. |
When set, storyboard draft rewrites the closing URL card to
<site.base_url>/<workspace slug> rather than trusting the host the model invented.
Unset, the model's text stands.