23 Configuration
This chapter lists everything you can set on a Crystalline instance: where the configuration is kept, which value wins when two places disagree, how to change a setting and every key and environment variable there is. Reach for it when a prose chapter names a setting in passing and you want its values, or when you set up a server and want the whole list in one place. The why stays in the prose chapters (chapter 5 for a laptop, Chapter 16 for a server, Chapter 17 and Chapter 19 for a team). This one keeps the complete list and links back to them.
23.1 Where configuration lives
Crystalline keeps three folders per user and uses the XDG layout on Linux and macOS alike, so on a Mac the file is under ~/.config too, never under ~/Library.
~/.config/crystalline/config.yaml-
The configuration file, or
$XDG_CONFIG_HOME/crystalline/config.yamlwhen that variable is set. It doesn’t have to exist: a missing file reads as all defaults, and a fresh install runs without one. ~/.local/state/crystalline-
The state folder (
$XDG_STATE_HOME), with the index inindex.db, the accounts inweb-auth.db, the daemon’sdaemon.logand each team domain’s origin state. Back up the whole folder with the knowledge, because three things in it exist nowhere else: the accounts, sessions, agent tokens and memberships inweb-auth.db, a virtual domain’s engrams inindex.dband the unshared drafts of a domain in review mode. ~/.cache/crystalline-
The cache (
$XDG_CACHE_HOME). The embedding model sits undermodels/there, unlessCRYSTALLINE_MODELS_DIRmoves it somewhere else.
On Windows the configuration and the state live under %APPDATA%\crystalline and the cache under %LOCALAPPDATA%\crystalline. The container image points all three at its /data volume, so there the file is /data/config/crystalline/config.yaml. serve, mcp, config and most other commands take --config <file> to read another file instead (verify --config is a different file, Chapter 26), and CRYSTALLINE_CONFIG does the same for every command in that environment; the flag wins over the variable.
Two more files share the name .crystalline.yaml, and neither is the configuration. One at a domain’s root tunes the checks for that domain (Chapter 26), and one in a workspace orders the domains the session-start briefing names there (Chapter 25).
23.2 Which value wins
The order is the same for every setting in the registry below: an environment variable beats the file and the file beats the built-in default (an empty variable counts as unset). A known variable with a value that can’t work stops the daemon at startup with a message naming the variable. The one exception is CRYSTALLINE_SERVICE_PUBLIC_URL, which only warns and falls back to deriving the address. An unknown CRYSTALLINE_* variable gets a warning in the log and is otherwise ignored.
Only five command-line flags override outright. serve --http and serve --allowed-host (repeatable) win over the variable and the file for that one run, and a serve whose flag disagrees with the saved value says so at startup. --config picks the file. The global --db picks the index file, ahead of database.url and the default index.db. --read-only on serve and mcp can only switch read-only on, never off, and on mcp it applies only when that process starts the daemon or runs the stack in-process.
Nothing from the environment is ever written back to the file. Crystalline ignores unknown keys in the file without a word, so watch the spelling: a misspelled key has no effect.
23.3 Changing a setting
crystalline config show prints every registry key with a one-line description and its effective value, marked (default) or (env) when it doesn’t come from the file. There’s no config get: show is the read, and the global --json makes it machine-readable. crystalline config set <key> <value> sets one key and crystalline config unset <key> puts it back to its default, removing a block that ends up empty. An unknown key is refused with the list of every known one, and a bad value is refused before anything is written.
With a daemon running, set and unset go to the daemon, which saves the file and takes the new value; with none, or with --config, the command edits the file directly. Either way the whole file is written out again, so comments, your own formatting and unknown keys are lost on the first set (a domain add rewrites it the same way). A file that doesn’t parse is never written over: set, unset, configure and the other commands that write the file refuse and leave it as it is until the YAML is fixed. The result tells you when a key only applies at the next daemon start, and also when an environment variable overrides the key, in which case the saved value waits until the variable is removed. A read-only instance refuses set and unset, and show still works there.
Three keys are secrets, database.url, auth.oidc.client_secret and automation.webhook.secret, so config show and crystalline doctor render them as (set) whatever they hold.
For an agent the same registry is the configure tool, without the operator settings: the network, the database, sign-in and the loops’ webhook, which means github.api_url, github.oauth_client_id, every auth, automation and database key and every service key except service.response_format. Those it neither lists nor changes, for any caller, and an operator changes them with config set, in the file or through the environment. Called with no arguments it returns the settings page, which any caller may read. set takes a map of keys to values and unset a list of keys; the keys are applied in ascending order, and the first bad one stops the rest while the ones before it stay applied. A call that names an operator setting is refused whole, before any key is applied. A change to an instance setting needs a stdio session, an anonymous HTTP session on an instance with auth.mcp off or an admin account. On a read-only instance the tool refuses even the bare call, although config show in a terminal works. domain points the call at one domain, as the next paragraph says. The other arguments, connect, token, host and restart, run the GitHub sign-in Chapter 17 walks through; host names a GitHub Enterprise Server for that one sign-in, and github.api_url is the lasting setting.
Called with domain, configure shows how that one domain behaves instead. It lists each MANIFEST policy with its value, the values it takes and whether you may change it. It says what each MANIFEST section does and which ones are missing, and it shows the rule overrides in the domain’s .crystalline.yaml beside every rule’s default severity. With set and unset it changes sharing, generated_indexes, read_only, reflection, reflect_trigger, evolve_trigger and the overrides: rules.<code> takes off, error, warning or info, and token_budget a whole number above 0. Every key is checked before anything is written, so one bad key writes nothing, and if writing .crystalline.yaml fails after the MANIFEST changed, the answer says what applied. The domain’s owner or an instance admin may change it, and on an instance with auth.mcp off so may an anonymous session wherever it may write, which is never a domain in review mode. An anonymous session never changes read_only, and configure switches it on but never off; while it’s on, the call changes nothing else (Section 25.7). In review mode a policy change lands as the owner’s or admin’s draft and a rule override is refused. A virtual domain takes no rule overrides, since it has no .crystalline.yaml. A domain from a connected server is changed on that server, and domain_name changes only through a rename (Chapter 18). Writing the file drops its comments.
The registry has 46 keys, and those are the only ones config set and the environment reach; configure reaches the 18 of them that aren’t operator settings. The embeddings block, prompt.rules and the domain registrations are edited in the file by hand or, for domains, with their own commands.
23.4 Serving
These keys shape the HTTP endpoint the daemon opens. All of them apply at the next daemon start except service.response_format, which a running daemon picks up at once. To restart, run crystalline ctl shutdown and let the next client start the daemon again, or restart the container or the systemd unit.
service.http-
The endpoint that carries MCP, the JSON API and Fluid, on at
127.0.0.1:7411by default.falseturns it off,truespells the default out and ahost:portbinds that address instead.serve --httpunderstandsonandofftoo, besides thetrueandfalsethatconfig settakes here. service.allowed_hosts-
A comma-separated list of extra
Hostheader values the endpoint accepts, the guard against DNS rebinding from Chapter 16. Loopback is always allowed and a single*allows any host; empty by default. With OAuth served, a request whoseHostisn’t on the list is refused rather than handed a document naming that host. service.public_url-
The address people open Fluid at, for example
https://knowledge.example.com, orhttps://example.com/crystallinefor a server under a path: an absolutehttporhttpsaddress with no query or fragment (0.0.0.0and::are refused). A path is the prefix the whole server answers under, and this key is the only thing that sets one. Its segments use lower-case letters, digits,.,_and-, and a trailing slash is dropped. An empty segment, a.or..segment, a backslash and a first segment the server answers at itself (api,assets,.well-knownand the other top-level paths of the server and of Fluid) are refused, andconfig setsays why. A bad value in the environment or the file is ignored with a startup warning, so the server answers at the root. Everyweb_urla tool hands back starts with this address, and it’s the OAuth resource identifier. Unset, the address is derived per caller, so set it behind a proxy that rewritesHost, in a container whose outside port differs from its inside one, for a separate Fluid deployment or for plainhttpoff loopback. service.ui-
Whether the endpoint serves Fluid,
trueby default. Withfalseonly the API and MCP remain, the shape a separately deployed Fluid sits in front of. service.api-
Whether the endpoint serves the JSON API under
/api/v1,trueby default.falsealso switches Fluid off and leaves MCP and/health. service.read_only-
Serves knowledge read-only,
falseby default. The tools that change content are hidden and refused while sync, watching and embedding keep running, andconfig setis refused;serve --read-onlyforces it on for one run. service.response_format-
Decides how list-shaped MCP results are encoded.
toon(the default) is compact for a model to read andjsonis plain JSON. skills.serve-
Whether the shipped skills are served over MCP (the
skillstool,skill://resources and the prompts).auto, the default, serves them to every client except a stdio session from a harness this machine already onboarded with hooks, which has them as files;truealways serves them andfalsenever does.
23.5 Signing in
Every key in this group applies at the next daemon start. Chapter 16 explains accounts, single sign-on and the header modes, and Chapter 19 what an agent is once it has signed in.
auth.anonymous-
Serves API requests that carry no identity at all, at viewer level. It’s
falseby default, and then such a request is answered401. auth.mcp-
Makes every MCP connection over HTTP present a personal token or sign in,
falseby default, while local stdio sessions are never asked. auth.oauth-
Serves OAuth for MCP clients, so a hosted client connects without a pasted token. Unset, it follows
auth.mcpwherever Fluid is served, andfalseturns it off. Withtrueit insists, and the daemon won’t start whileauth.mcp,service.apiorservice.uiis off. auth.max_users-
How many accounts the trusted header, the forward-auth headers and single sign-on may create in total, 100 by default and at least 1. Past the cap only a new account is refused, and
crystalline usersis never capped. auth.login.free_attempts- How many failed sign-ins in a row one name may make before each further attempt is delayed, 3 by default. The count is kept per submitted name, whether or not that account exists, and a successful sign-in clears it.
auth.login.max_delay-
The longest delay in seconds, 8 by default: the delay doubles from one second up to this, and after that the answer is
429with aRetry-After. Set it to0and the whole throttle is off. auth.trusted_header-
The one header a trusted proxy sets to name the signed-in user, for example
remote-user. Unset by default, when no header is believed; an account it names is created at viewer role the first time it’s seen. auth.proxy_headers-
Trusts the forward-auth set
Remote-User,Remote-Name,Remote-EmailandRemote-Groups,falseby default, and new accounts land atauth.oidc.default_role. Setting it together withauth.trusted_headerkeeps the HTTP endpoint from starting, so pick one. auth.oidc.issuer-
The single sign-on provider’s issuer URL, the one discovery appends
/.well-known/openid-configurationto; while it’s unset, single sign-on is off. Entra’s{tenantid}template and thecommon,organizationsandconsumersendpoints are refused. auth.oidc.client_id- The client id the provider issued when you registered this instance with it.
auth.oidc.client_secret-
The secret that goes with the client id, best kept in
CRYSTALLINE_AUTH_OIDC_CLIENT_SECRET, orCRYSTALLINE_AUTH_OIDC_CLIENT_SECRET_FILEfor a Docker secret, rather than set withconfig set: that keeps it out of the file and out of your shell history. auth.oidc.name- The label on the sign-in button, which uses generic wording while this is unset.
auth.oidc.scopes-
The scopes requested, separated by spaces. Leave it unset and you get
profile email;openidis always added. auth.oidc.default_role-
Decides the role a first sign-in creates an account at. It takes
none,viewer, the default,editororadmin, and it also applies to accounts that arrive throughauth.proxy_headers. auth.oidc.redirect_uri-
The callback the provider sends the browser back to, used verbatim instead of the address derived from each request. It’s an absolute
httpsURL (httponly on loopback) ending in/api/v1/auth/oidc/callback, spelled exactly as the provider has it registered. Under a path it carries the path ofservice.public_urlin front,https://example.com/crystalline/api/v1/auth/oidc/callback, andconfig setrefuses a value whose path disagrees withservice.public_url. A value in the file that can’t work, a disagreeing path included, leaves single sign-on off with a warning at startup.
23.6 Team sync
Changes to the github.* keys take effect at once, without a restart. Chapter 17 tells the story of team domains, and a domain’s own origin (repository, branch and poll interval) is part of its registration in Chapter 25. A server’s own GitHub App (Chapter 16) is no registry key: it’s created in Fluid, saved with crystalline github app set (status shows it and remove deletes it) or given through the CRYSTALLINE_GITHUB_APP_* variables below.
github.enabled-
Turns team collaboration on,
falseby default. It lists the six collaboration tools and starts the background poll, and connecting GitHub from Fluid turns it on as well. github.poll_secs- Sets how often the daemon checks each team domain’s repository for changes, in seconds: 300 by default and at least 60.
github.stacks-
Whether sharing again while a proposal is open stacks a new proposal on top of it where the forge supports stacked pull requests,
trueby default. Withfalsethere’s one proposal per domain, updated in place. github.share_identity-
Whose GitHub identity a share runs under.
instance, the default, uses the instance’s own GitHub credential for everything, the one connected token or, on a server with a GitHub App, the App, which then opens each proposal and names the person who shared as the commit author.personalmakes each person connect their own identity for sharing (on a server with an App, their GitHub link), while pulling stays on the instance’s credential.config setrefuses any other value, and a hand-edited one reads asinstance. github.agent_identity-
In
personalmode, the account whose connected GitHub identity an agent’s share over HTTP runs under when agents don’t sign in (auth.mcpoff). While it’s unset, such shares are refused. On a server with a GitHub App it does more: the open tier acts on every team domain as this account, with its GitHub role but never above editor, private domains included, and without it the open tier is an anonymous visitor there. Name an account that isn’t an instance admin (crystalline doctorwarns when it is one), or better, turnauth.mcpon. The account name takes lowercase letters, digits,.,_and-. github.api_url-
The GitHub API address,
https://api.github.comby default; point it at your GitHub Enterprise Server’s API to use one. github.oauth_client_id-
The client id of an OAuth App you registered yourself, which replaces the one built in. Set it together with
github.api_urlwhen the sign-in should use an app on your own server.
23.7 Search, capture and recall
These keys take effect at once, and Chapter 9 explains how a search ranks.
search.salience_weight- The largest lift a fully salient engram gets in the ranking, 0.0 to 1.0, 0.15 by default. It reorders results within a band of relevance and never filters.
search.retired_weight-
The factor applied to an engram whose status is
deprecated,superseded,archivedorlegacy, 0.0 to 1.0, 0.6 by default;1.0turns the fade off. capture.similar-
Whether a write, or an edit that changes content, answers with the nearest existing engrams and the advice to merge, supersede or link them,
trueby default.falseturns the hint off for agents and in Fluid alike. evolve.contradictions-
Whether the daemon reads the observation lines of related engrams with a local model and raises
V302for two engrams that may contradict each other,offby default orfull. A finding is a question, never a verdict, because the model can be wrong.fulldownloads the model on first use (about 550 MB on disk), and while it’s loaded it takes about 2 to 3 GB of memory. recall.enabled-
Whether the prompt hook
crystalline installwires puts the engrams that may apply in front of the agent with each prompt,trueby default. It works in Claude Code and Codex; Copilot gets the hook too, but there it stays quiet.falsekeeps the hook installed and silent. recall.limit- How many engrams one prompt may bring along, 1 to 5, 3 by default; the rest wait for a later prompt on the same topic.
recall.min_score- The search score an engram must reach before the hook names it, 0.0 to 1.0, 0.69 by default. Raise it for a quieter hook and lower it to hear about weaker matches.
remote.deadline_ms- How long a search, or another call over every domain, waits for each connected server, in milliseconds, 100 to 60000, 3000 by default. A server that takes longer is left out of that answer and named in it (Section 16.10).
23.8 The loops’ webhook
Three operator settings say where and when an instance calls a runner when a domain’s loop has work (Chapter 14). A domain only asks for the call through its trigger keys (Chapter 24); where the call goes stays the instance’s choice, since a MANIFEST travels with a shared domain.
automation.webhook.url-
Where the instance posts the signed webhook, an
http://orhttps://address, unset by default. Unset means no call goes out, whatever a domain’s triggers say. One address serves both loops and every domain, and the body names the loop and the domain. automation.webhook.secret-
The secret the body is signed with, so the receiver can check that the instance sent it. It’s shown only as
(set), andCRYSTALLINE_AUTOMATION_WEBHOOK_SECRETor its_FILEform keeps it out of the file. A URL without a secret never sends, and the loop’s status says so. automation.evolve_threshold- How many mechanical findings make the evolve trigger call, a whole number from 1 up, 20 by default. A run is due from the first finding; the threshold only decides when the server calls.
23.9 Embeddings
The embedding model turns text into the vectors semantic search compares. It’s configured in an embeddings block that isn’t in the registry: you edit it in the file by hand, it has no environment twin and config set doesn’t know it. The daemon reads it when it starts, so restart after a change. A block needs both provider and model, and a block missing either stops the whole file from loading.
embeddings.provider-
local(the default) runs the model inside the daemon, andopenai-compatible(also spelledopenaiorremote) sends the text to an endpoint that speaks the OpenAI embeddings API. embeddings.model-
For
local,granite-embedding-97m-multilingual-r2by default, a multilingual model of about 220 MB, orbge-small-en-v1.5, the English model used before 0.19.0, about 130 MB. Either the short name or the full repository name works. For a remote provider it’s whatever name the endpoint gives its model. embeddings.endpoint-
The base address of a remote provider, which won’t start without it; the daemon posts the text to
<endpoint>/embeddings. embeddings.api_key_env- The name of the environment variable that holds the remote provider’s key, never the key itself; if that variable isn’t set, the provider fails to start.
An install without the block downloads the multilingual model on first start. Write the block yourself to keep an index on the English model or to use a remote endpoint:
embeddings:
provider: local
model: bge-small-en-v1.5embeddings:
provider: openai-compatible
model: my-embedding-model
endpoint: https://embeddings.example.com/v1
api_key_env: EMBEDDINGS_API_KEYSwitching the model re-embeds every engram in the background. If the provider can’t be built (a missing key, an unknown model), the daemon logs why and carries on with full-text search only. crystalline model download fetches the configured local model ahead of time, for a host that won’t have network access later.
23.10 Storage
These keys decide where the index lives. For a file domain the index is derived from the files and can always be rebuilt, while a virtual domain’s engrams live in the index itself. The two database keys wait for the next daemon start, while index.files and domains_root take effect at once.
database.backend-
The store behind the index:
turso, the embedded default, orpostgres. Several instances can share one PostgreSQL database with pgvector, each file domain hosted by one of them, as docs/deployment.md describes under shared database collaboration. Upgrade every instance of a shared database together, since an older one left running writes rows the newer version reads differently: stop them all and start them again on the new version, and they take turns on the migration. database.url-
For
postgres, the connection URL, startingpostgres://orpostgresql://. Point it at Postgres directly or at a pooler in session mode, never at one in transaction mode, because the instances take short per-domain locks that a transaction-mode pooler breaks. Forturso, an optional path for the index file, below--dband above the default. index.files-
Whether Crystalline keeps a generated
index.mdin every folder of a file domain,trueby default. Set it tofalseand Crystalline stops generating them, leaving the ones already there where they are and out of the index. Either way they stay on your machine unless the domain’s MANIFEST shares them (Chapter 24). domains_root-
The folder a new file domain lands under when you give no path,
~/Documents/Crystallineby default. In a container, point it at a persistent path so domains created in Fluid survive.
Accounts don’t move to Postgres. web-auth.db stays a local file in each node’s state folder whatever the backend, and crystalline reindex --wipe refuses on Postgres. Each domain’s own entry in the file (domains.<name>, with its path, origin, review mode and provisioning decision) is written by the domain commands, and Chapter 25 lists its keys.
On a shared database one instance hosts each file domain: it syncs and watches the files and serves the domain’s Reflection, and the others name it when they refuse a write. A host that has gone silent for 90 seconds is taken over by the next write that reaches another instance. To move sooner, crystalline serve --take-over takes every file domain the instance has, and crystalline sync --domain <name> --take-over takes one. Each instance judges silence by its own clock, so keep the instances’ clocks in sync. The files are the domain’s truth, so copy the domain folder to the new host first, or keep it on a volume both share. Until it’s there, crystalline status names the files that exist only on the old host. If they’re gone for good, crystalline sync --domain <name> --accept-disk lets them go.
23.11 Identity and prompt rules
identity.actor and prompt.rules decide whose name a write carries and which domains a session hears about. The first is a registry key like the ones above, the second a hand edit in the file.
identity.actor-
Who is recorded as the writer in
generated.byon every write, for exampleteam-bot/1.0orhuman:dallas, with no spaces. A change takes effect at once, and while it’s unset the connected client names itself. prompt.rules-
Which domains the session-start briefing names, depending on the folder a session starts in. It’s a hand edit with no environment twin, and it applies from the next session on. The rules cover a connected server’s domains in the briefing too. Each key is a path pattern, with
~expanded, and each value anincludelist, anexcludelist or both.
Every rule whose pattern matches the workspace counts. The included domains of all matching rules add up and the excluded ones are taken away; a match with no include starts from every domain. When no rule matches, or there are no rules, the briefing names every domain. A pattern ending in /** matches the folder itself as well as everything under it, so a session started right in ~/work names engineering and payments. In ~/work/website or any folder below it both rules match: the first includes the two domains, the second takes payments away, and the briefing names engineering only.
prompt:
rules:
"~/work/**":
include: [engineering, payments]
"~/work/website/**":
exclude: [payments]23.12 Environment variables
Every one of the 46 registry keys has an environment twin: CRYSTALLINE_ followed by the key in capitals, with dots turned into underscores. github.enabled becomes CRYSTALLINE_GITHUB_ENABLED, domains_root becomes CRYSTALLINE_DOMAINS_ROOT, remote.deadline_ms becomes CRYSTALLINE_REMOTE_DEADLINE_MS and the login throttle is CRYSTALLINE_AUTH_LOGIN_FREE_ATTEMPTS and CRYSTALLINE_AUTH_LOGIN_MAX_DELAY. A twin takes the same values config set does and is checked the same way, which is how a container gets configured without a file. A domain registers from the environment too, through CRYSTALLINE_DOMAIN_<NAME> and its _ORIGIN, _REVIEW and _READERS suffixes, for this process only, and so does a connected server, through CRYSTALLINE_REMOTE_URL, CRYSTALLINE_REMOTE_TOKEN and, to take only some of its domains, CRYSTALLINE_REMOTE_DOMAINS (Chapter 25). On Windows a daemon started by the sign-in task doesn’t see a variable set only in your terminal, so set it as a user environment variable, or stop the daemon and start it from that shell.
The OIDC client secret, the webhook secret, the GitHub token, the GitHub App’s private key, client secret and webhook secret and the remote token can also come from a file such as a Docker secret, through CRYSTALLINE_AUTH_OIDC_CLIENT_SECRET_FILE, CRYSTALLINE_AUTOMATION_WEBHOOK_SECRET_FILE, CRYSTALLINE_GITHUB_TOKEN_FILE, CRYSTALLINE_GITHUB_APP_PRIVATE_KEY_FILE, CRYSTALLINE_GITHUB_APP_CLIENT_SECRET_FILE, CRYSTALLINE_GITHUB_APP_WEBHOOK_SECRET_FILE and CRYSTALLINE_REMOTE_TOKEN_FILE, and so can the first admin and the setup token below. The file loses one trailing line break, and setting both forms, or giving a file that can’t be read or is empty, stops the start. A handful of other variables cover what the registry doesn’t:
CRYSTALLINE_ACCELERATION-
offkeeps the embedding model and the contradiction check’s model on the CPU on an Apple Silicon Mac, where they otherwise run on the GPU through Metal. Any other value, or none, leaves the GPU on, and other platforms always use the CPU. CRYSTALLINE_ADMIN_NAMEandCRYSTALLINE_ADMIN_PASSWORD_FILE-
The first admin, created at the first
servestart while the instance has no account yet and ignored after that (Chapter 16). The password file is a Docker secret; the plainCRYSTALLINE_ADMIN_PASSWORDworks too but shows indocker inspect, andCRYSTALLINE_ADMIN_NAME_FILEtakes the name from a file. A missing half stops the first start with a message naming the variable. CRYSTALLINE_CONFIG-
Another configuration file for every command in that environment, with
~expanded;--configwins over it. CRYSTALLINE_GITHUB_TOKEN-
A GitHub token for a machine that can’t sign in interactively. While it’s set, a saved sign-in is ignored and
crystalline connect githubrefuses. CRYSTALLINE_GITHUB_APP_ID,CRYSTALLINE_GITHUB_APP_CLIENT_IDandCRYSTALLINE_GITHUB_APP_PRIVATE_KEY-
The server’s own GitHub App, for infrastructure as code (Chapter 16): its numeric id, its client id and its PEM private key, all three together, or the start stops naming the one that’s missing.
CRYSTALLINE_GITHUB_APP_CLIENT_SECRETadds the client secret that linking in the browser needs, andCRYSTALLINE_GITHUB_APP_WEBHOOK_SECRETthe secret GitHub signs its webhook with. The key and both secrets are shown only as(set)and have_FILEforms. The variables win over an App saved withcrystalline github app setor created in Fluid, nothing from them is written anywhere, and while they’re setgithub app setandgithub app removerefuse. CRYSTALLINE_MODELS_DIR-
Where the embedding model lives instead of the cache folder, for a host without network access or an image with the model built in (the
with-modelimage sets it). Give two daemons with different models a folder each, or each deletes the other’s model on every start. CRYSTALLINE_SETUP_TOKENandCRYSTALLINE_SETUP_TOKEN_FILE-
The one-time token the create-first-admin form asks for off loopback, chosen by you instead of minted by
serve(Chapter 16). It needs at least 32 characters or the daemon won’t start, it’s never printed (serveonly says which variable it came from) and it stops working once the first admin exists. CRYSTALLINE_UPSTREAMandCRYSTALLINE_BASE_PATH-
Tell the separate Fluid image where its web server forwards
/api/, ashost:port,crystalline:7411by default.CRYSTALLINE_BASE_PATHserves it under a path such as/crystalline(one leading slash, no trailing one) instead of the root; give the daemonservice.public_urlwith the same path. Crystalline itself never reads either.
XDG_CONFIG_HOME, XDG_STATE_HOME and XDG_CACHE_HOME move the three folders from the top of this chapter, and on Windows APPDATA and LOCALAPPDATA do the same. COPILOT_HOME tells crystalline install where the Copilot CLI keeps its files, ~/.copilot by default.
RUST_LOG-
Sets how much the daemon and every command log, as a filter like
RUST_LOG=debugorRUST_LOG=crystalline_engine=debug,warn. A log line names the part of Crystalline it comes from (crystalline_engine,crystalline_rest,crystalline_identityorcrystalline_service), and since 0.20.0crystalline_servicecovers only the daemon’s own lines. Without itservelogs atinfoand everything else atwarn. The log goes to stderr only, so the routing block, hook output and the MCP stream stay clean, and a hook logs only when the variable is set.
The configuration is one YAML file per user under ~/.config/crystalline, beside a state folder that holds the index and the accounts and a cache that holds the model. For the 46 registry keys an environment variable beats the file and the file beats the default, only five flags override outright, and config set changes a key while rewriting the whole file, as configure does for the keys an agent may change. Keys under auth, database and most of service apply at the next daemon start, as do skills.serve and the embeddings block; prompt.rules applies from the next session and the other keys take effect at once. The embeddings block, prompt.rules and the domain registrations are edited by hand or with their own commands and have no environment twin, while every registry key does.