Skip to main content

Environment variables

Environment variables set for Claude Code subprocess invocations and skill CLI commands.

Core variables​

Set for every task:

VariableSource
ISTOTA_TASK_IDTask ID
ISTOTA_TASK_ATTEMPTWhich attempt of that task is running, 1-based (attempt_count + 1), matching the session log's file name. tasks transcript reads it to exclude the transcript the calling run is still writing — a fact about the process, so it is fixed in the environment rather than re-read from a row the stuck-task reaper mutates. Handed to skill CLIs via the proxy; never present in the model's own environment, since it is what the exclusion trusts
ISTOTA_USER_IDTask's user ID
ISTOTA_DB_PATHFramework database path. Handed to skill CLIs via the proxy for every user; never present in the model's own environment
ISTOTA_CONVERSATION_TOKENTalk room token (if set)
ISTOTA_DEFERRED_DIRTemp directory for deferred JSON writes
ISTOTA_SKILL_PROXY_SOCKSkill proxy socket path (if proxy enabled)
ISTOTA_SKILL_CLIENT_WAITsecurity.skill_client_wait_seconds, set alongside the socket when the proxy is enabled. The sandboxed istota-skill client cannot read a config, so this is how an operator's value reaches it; it falls back to 600 when unset. The proxy derives its own ceiling from the config field and never from this export, so a task rewriting its copy changes only its own patience
ISTOTA_SANDBOXED1 when the task is really running under bwrap (sandbox and skill proxy both enabled and bwrap present), otherwise unset. istota-skill reads it to fail closed instead of running a skill module in-process: inside the sandbox the databases are masked out, so a direct run would report a missing table rather than the misconfiguration it is
ISTOTA_BOT_DIR_NAMEconfig.bot_dir_name — the per-user bot directory (Users/<user>/<bot_dir_name>/) skills write into
ISTOTA_CONFIG_PATHConfig file path (propagated to subprocess children so module-skill jobs find the config)
ISTOTA_EXPERIMENTAL_FEATURESCSV of enabled experimental features (config.experimental.features). Injected by every subprocess builder so @requires_feature-gated CLI subcommands and gated skills see the same gate as the LLM path
SHELLOPTSFixed to pipefail (shell_exec.pipefail_env), applied last by build_clean_env. Bash imports it at startup, so every bash below a task has the option on — including the ones istota never sees, which is the point: a claude_code or tmux_claude task runs its commands through the Claude Code CLI's own Bash tool (bash -c 'source <snapshot> && eval <cmd>'), a process istota launches but does not instrument, and that shell started with pipefail off and reported a pipeline's last stage (ISSUE-321). SHELLOPTS rather than BASH_ENV because it names shell options and cannot name a file to source, so it opens no exec inlet — pipefail:$(touch /tmp/x) is rejected as an invalid option name rather than evaluated. Being inherited rather than a flag, it also reaches a pipeline inside a nested bash script.sh, which -o pipefail does not; it reaches nothing that is not bash. BASH_ENV, SHELLOPTS and BASHOPTS are stripped from the inherited environment by both env builders first, so no value from outside survives to be trusted

Two variables belong to the daemon's own environment (build_stripped_env) rather than to a task, so they are not in the table above:

  • PRECOMMIT_SCANS_REQUIRED=1 on cron command jobs and heartbeat shell commands, so the pre-commit scans refuse rather than warn where they cannot run. A model task is recognised as unattended by ISTOTA_SANDBOXED / DEVELOPER_REPOS_DIR instead, and those are built per task. See secret scanning.
  • No SHELLOPTS. Those two paths take pipefail from shell_exec.shell_argv's bash -o pipefail -c instead — flag depth only, deliberately, because the commands there are operator-authored rather than model-authored.

Nextcloud​

VariableSource
NC_URLconfig.nextcloud.url
NC_USERconfig.nextcloud.username
NC_PASSconfig.nextcloud.app_password
ISTOTA_WORKSPACE_PATHconfig.workspace_path (scoped to user dir for non-admin)
NEXTCLOUD_MOUNT_PATHconfig.workspace_path (compatibility alias)

CalDAV​

Derived from Nextcloud credentials:

VariableSource
CALDAV_URLconfig.nextcloud.url + /remote.php/dav
CALDAV_USERNAMEconfig.nextcloud.username
CALDAV_PASSWORDconfig.nextcloud.app_password

Email​

VariableSource
SMTP_HOSTconfig.email.smtp_host
SMTP_PORTconfig.email.smtp_port
SMTP_USERconfig.email.effective_smtp_user
SMTP_PASSWORDconfig.email.effective_smtp_password
SMTP_FROMPlus-addressed: bot+user_id@domain
IMAP_HOSTconfig.email.imap_host
IMAP_PORTconfig.email.imap_port
IMAP_USERconfig.email.imap_user
IMAP_PASSWORDconfig.email.imap_password

Browser​

VariableSource
BROWSER_API_URLconfig.browser.api_url
BROWSER_VNC_URLconfig.browser.vnc_url

Service integrations​

Every service-integration env var is declared in the consuming skill's skill.md env: block and resolved by build_skill_env() against the per-task EnvContext. Per-user credentials come from the encrypted secrets table (from: "secret"); module-skill subprocesses receive ISTOTA_SECRET_KEY via the proxy so they can decrypt in-process.

VariableSourceNotes
KARAKEEP_BASE_URLsecrets (karakeep.base_url)per-user
KARAKEEP_API_KEYsecrets (karakeep.api_key)per-user, sensitive
MONARCH_SESSION_IDsecrets (monarch.session_id)per-user, sensitive
MONARCH_CSRFTOKENsecrets (monarch.csrftoken)per-user, sensitive
FEEDS_USERtask user_iddeclared in the feeds skill.md env spec (from: user_id) and resolved by build_skill_env in every subprocess path (LLM execute_task, skill-task, command-task)
TUMBLR_API_KEYsecrets (feeds.tumblr_api_key)per-user, sensitive
NTFY_TOPIC / NTFY_SERVER_URL / NTFY_USERNAMEsecrets (ntfy.*)per-user (non-credential)
NTFY_TOKEN / NTFY_PASSWORDsecrets (ntfy.token / ntfy.password)per-user, sensitive
MONEY_USERtask user_idthe only money env var; config is resolved from the per-user money DB via resolve_for_user. MONEY_CONFIG / MONEY_SECRETS_FILE and the standalone money binary are gone — money is fully istota-native (reachable as istota money …).

Module setup_env hooks​

Some module env vars are resolved at runtime by Python hooks rather than static config lookups. These are declared from: "setup_env" in the skill manifest and dispatched by dispatch_setup_env_hooks in the scheduler, command-task, skill-task, and heartbeat paths.

VariableSourceNotes
HEALTH_DB_PATHistota.health.resolve_for_user(user_id, config).db_pathper-user; no-op when health module is disabled
LOCATION_DB_PATHistota.location.resolve_for_user(user_id, config).db_pathper-user; no-op when location module is disabled

Google Workspace​

VariableSource
GOOGLE_WORKSPACE_CLI_TOKENOAuth access token from DB (injected via setup_env() hook, auto-refreshed)

Developer​

VariableSource
DEVELOPER_REPOS_DIR{config.developer.repos_dir}/{user_id}, derived by the developer skill's setup_env hook. Admin tasks only — it is the subtree the sandbox binds, and a non-admin has no bind behind it.
GITLAB_URLconfig.developer.gitlab_url
GITLAB_DEFAULT_NAMESPACEconfig.developer.gitlab_default_namespace
GITLAB_REVIEWERconfig.developer.gitlab_reviewer
GITHUB_URLconfig.developer.github_url
GITHUB_DEFAULT_OWNERconfig.developer.github_default_owner
GITHUB_REVIEWERconfig.developer.github_reviewer
DEVELOPER_AUTHOR_CREDITconfig.developer.author_credit
GIT_CONFIG_*Git credential helpers for HTTPS auth
GH_HOST, GITLAB_HOSTWritten by the forge wrapper into the real CLI's environment, derived from the two URLs
ISTOTA_PATH_PREPENDInternal. The task's {user_temp_dir}/.developer directory, which holds the gh / glab wrappers, plus .developer/exec-shims where development work runs in the devbox ([developer] enabled, [developer] repos_dir and [devbox] enabled all set); the executor folds them onto PATH and strips the variable before the model sees it

Package caches​

Set only where the filesystem sandbox is really in force, and only when a cache root resolves for the task's user — with [developer] enabled and a repos_dir that is {repos_dir}/{user_id}/.package-caches, derived rather than configured; otherwise {sandbox_cache_dir}/{user_id}. Without them a task's downloads land on bubblewrap's root tmpfs, which is RAM (ISSUE-305). What is left behind is bounded by sandbox_cache_sweep_interval and sandbox_cache_max_gb.

VariableSource
UV_CACHE_DIR{cache root}/uv
npm_config_cache{cache root}/npm — npm on Linux uses ~/.npm and ignores XDG, so XDG_CACHE_HOME alone would leave it in RAM
XDG_CACHE_HOMEThe cache root itself, so a third tool's cache lands there too. It counts against the budget while neither sweep verb can touch it
HF_HOMEPinned back to $HOME/.cache/huggingface, not moved with XDG. It defaults to $XDG_CACHE_HOME/huggingface, so moving XDG would orphan the read-only pre-warmed model bind and every task would re-download it

Devbox​

Set when [devbox] enabled. The exec socket directory is bound into a sandbox only when developer is in the task's authorized skills.

VariableSource
ISTOTA_DEVBOX_CONTAINER{devbox.container_prefix}{user_id}
ISTOTA_DEVBOX_DOCKER_CLIconfig.devbox.docker_cli
ISTOTA_DEVBOX_MAX_OUTPUT_BYTESconfig.devbox.max_output_bytes

There is deliberately no ISTOTA_DEVBOX_EXEC_TIMEOUT: the transport imposes no timeout, the task's own budget governs, and a caller wanting a kill passes --timeout. The exec protocol carries no env field either — a task's environment is never forwarded into the container, whose caches are set on the container itself.

Credential proxy​

When skill_proxy_enabled = true, every env var declared with sensitive: true in any skill manifest is stripped from the subprocess environment and injected server-side by the proxy. The set is computed at task time by derive_credential_set(skill_index). Today's set:

  • CALDAV_PASSWORD
  • NC_PASS
  • SMTP_PASSWORD
  • IMAP_PASSWORD
  • KARAKEEP_API_KEY
  • GOOGLE_WORKSPACE_CLI_TOKEN
  • GITLAB_TOKEN
  • GITHUB_TOKEN
  • MONARCH_SESSION_ID, MONARCH_CSRFTOKEN
  • NTFY_TOKEN, NTFY_PASSWORD
  • TUMBLR_API_KEY
  • ISTOTA_BRAIN_NATIVE_API_KEY — declared by code_review, which calls a model itself
  • ISTOTA_SECRET_KEY — routed to module-skill subprocesses, hard-blocked at the lookup endpoint via _PROXY_LOOKUP_BLOCKED

The proxy injects each credential only into the skill CLIs whose manifest declared it (derive_skill_credential_map). Authorization is based on credential presence in the task env — not skill selection — so any skill whose credentials the user has configured can request them at runtime. See security: credential proxy for the authorization model and rejection logging. See credentials for the full two-tier credential inventory and provisioning guide.

Secret overrides​

These env vars override TOML config values (for use with systemd EnvironmentFile=):

Env varConfig field
ISTOTA_NEXTCLOUD_APP_PASSWORDnextcloud.app_password
ISTOTA_CALDAV_PASSWORDcaldav.password (the no-Nextcloud shape's calendar credential)
ISTOTA_EMAIL_IMAP_PASSWORDemail.imap_password
ISTOTA_EMAIL_SMTP_PASSWORDemail.smtp_password
ISTOTA_DEVELOPER_GITLAB_TOKENdeveloper.gitlab_token
ISTOTA_DEVELOPER_GITHUB_TOKENdeveloper.github_token
ISTOTA_GOOGLE_WORKSPACE_CLIENT_SECRETgoogle_workspace.client_secret
ISTOTA_WEB_OAUTH2_CLIENT_SECRETweb.oauth2_client_secret
ISTOTA_WEB_SESSION_SECRET_KEYweb.session_secret_key
ISTOTA_BRAIN_NATIVE_API_KEYbrain.native.api_key (native brain provider key; kept out of TOML)
ISTOTA_SMS_TWILIO_ACCOUNT_SIDsms.twilio.account_sid
ISTOTA_SMS_TWILIO_AUTH_TOKENsms.twilio.auth_token
ISTOTA_SMS_TWILIO_API_KEY_SIDsms.twilio.api_key_sid
ISTOTA_SMS_TWILIO_API_KEY_SECRETsms.twilio.api_key_secret
ISTOTA_SMS_TWILIO_MESSAGING_SERVICE_SIDsms.twilio.messaging_service_sid
ISTOTA_SMS_TELNYX_API_KEYsms.telnyx.api_key
ISTOTA_SMS_TELNYX_PUBLIC_KEYsms.telnyx.public_key
ISTOTA_SMS_TELNYX_MESSAGING_PROFILE_IDsms.telnyx.messaging_profile_id
ISTOTA_WHATSAPP_ACCESS_TOKENwhatsapp.cloud.access_token
ISTOTA_WHATSAPP_APP_SECRETwhatsapp.cloud.app_secret
ISTOTA_WHATSAPP_VERIFY_TOKENwhatsapp.cloud.verify_token

The three WhatsApp names kept their un-nested spelling when the block moved under cloud, so an existing secrets.env keeps working across that upgrade. Each variable is listed in full rather than as a shared prefix and a list of suffixes: this is the page you reach by searching for the variable already sitting in your secrets.env, and a suffix matches nothing.

See credentials for what each override covers and the full env var → config mapping.