Skip to main content
How to configure a skillscript-runtime deployment. The single config file is ~/.skillscript/connectors.json (or any path passed via --connectors). It has two top-level concerns:
  1. substrate — which SkillStore, DataStore, LocalModel, and AgentConnector the runtime hosts (MCP server + web dashboard) use.
  2. Named MCP connector instancesyoutrack, github, etc. — invoked via $ <name> in skill source.
The runtime loads connectors.json at startup. Missing file → graceful empty config (substrate defaults to filesystem skills + conditional sqlite memories; no MCP connectors). Malformed JSON or unknown fields → structured errors surfaced at bootstrap.

SKILLSCRIPT_HOME — the root override

Every default path the runtime computes is rooted under SKILLSCRIPT_HOME: SKILLSCRIPT_HOME defaults to ~/.skillscript. Set the env var to relocate everything under a different root — the cleanest multi-instance isolation primitive:
Every derived path now lives under ~/.skillscript-adopter/; the dev instance at ~/.skillscript/ is untouched. No --connectors flag, no explicit dbPath overrides needed — defaults follow SKILLSCRIPT_HOME. See docs/adopter-playbook.md § “Two-instance posture” for the broader pattern.
Why this matters for adopter setups. Without SKILLSCRIPT_HOME isolation, two daemons running side-by-side would share triggers.json, skillsDir (filesystem default), and any other $HOME/<thing> default — even if their sqlite dbPaths were explicitly distinct. SKILLSCRIPT_HOME is the architectural primitive; everything else derives from it.

Environment variables + .env file

The CLI auto-loads $SKILLSCRIPT_HOME/.env at startup and populates process.env for any key not already set in the shell. Drop a .env next to skillscript.config.json and posture switches are picked up at next restart — the installer/operator pattern.

Direct env-var reads — the runtime checks process.env for these

SKILLSCRIPT_HOME is the chicken-and-egg case — the path to .env requires it, so .env can’t set it. Use shell, Docker -e, or systemd Environment= instead.

Indirect via ${VAR} substitution in config files

Once .env populates process.env, both skillscript.config.json (runtime-config.ts) and connectors.json (connectors/config.ts) resolve ${VAR} references in string values at load time. So a .env-set var flows into:
  • skillscript.config.json — any string field. Examples: dashboard.host: "${BIND_HOST}", triggersFilePath: "${TRIGGERS_PATH}".
  • connectors.json — the big one for adopter wiring. Endpoints, auth tokens, child-process env blocks. Example:
    MEMORY_ENDPOINT and MEMORY_TOKEN in .env; declarative shape committed to connectors.json.

.env file format

Standard dotenv conventions:
Supported: KEY=value, quoted strings (double + single), # comment lines, blank lines, embedded equals signs in values. Rejected (logged as warnings, skipped): malformed entries without =, invalid key names. Missing file → no-op. NOT supported (deliberately — use JSON config or shell-escape for these): multi-line values, variable interpolation within values (${OTHER_VAR} inside a value), export KEY=value prefix, inline comments after a value.

Precedence summary (most-specific wins)

  1. CLI flag (e.g., --port 8080)
  2. Shell-set env var (export SKILLSCRIPT_PORT=8080)
  3. .env file in $SKILLSCRIPT_HOME
  4. skillscript.config.json field
  5. Built-in default

skillfile init seeds .env.example

Running skillfile init writes $SKILLSCRIPT_HOME/.env.example documenting every recognized env var. Operators copy to .env and edit. Re-running init never overwrites operator-edited .env — only writes the template.

Adopter credential discipline

  • Commit connectors.json with ${VAR} references; never literal secrets.
  • .gitignore .env (the file with real values); commit .env.example (the template).
  • See Credential discipline below for the broader pattern.

Quick start

A typical out-of-the-box ~/.skillscript/connectors.json:
Equivalent to omitting the file entirely — these are the base config defaults. agent_connector: null falls back to the silent NoOpAgentConnector — skills with # Output: agent: X complete cleanly with a stderr warning; replace with "noop" for the same behavior stated explicitly, or with a "custom" entry to wire an adopter impl. To switch skills storage to SQLite:
Restart skillfile dashboard (or skillfile serve). The MCP server + dashboard UI now read/write skills from ~/.skillscript/skills/skills.db instead of .skill.md files.
Heads up on startup logs. Sqlite-backed substrates use the built-in node:sqlite module, which is still flagged experimental in Node 22. Expect this line on every launch until Node de-experimentalizes it: ExperimentalWarning: SQLite is an experimental feature and might change at any time. Harmless; can be silenced per-process with NODE_OPTIONS="--disable-warning=ExperimentalWarning" if it clutters your logs.

The substrate section

Singleton substrate connectors. Each slot accepts one of four shapes:

Short form — bare string

Wires the bundled implementation for that type with default config (e.g., dbPath under ~/.skillscript/). Valid short-form values per slot:

Null — explicit “no substrate”

The runtime doesn’t register a real connector for this slot. local_model: null leaves $ llm un-wired (skills calling it error at execute time). agent_connector: null falls back to NoOpAgentConnector# Output: agent: declarations complete with a stderr warning instead of throwing, so a runtime can start without any agent harness wired.

Object form — override defaults

type picks the bundled impl; config is passed to its constructor. Per-type config fields:
SqliteDataStore feature surface. The bundled sqlite data_store is a deliberately minimal reference implementation: supports_writes + supports_tag_filter are true; supports_semantic, supports_pinning, supports_decay_model, supports_thread_status_filter are all false. Rich features (semantic retrieval, pinning, decay scoring, thread-status workflow) come from substrate impls — adopters fork examples/connectors/DataStoreTemplate/ and wire their backing system (memory broker, vector DB, AMP, etc.). The bundled impl exists so the runtime works out-of-box; adopters with richer query semantics write their own.
Worked Ollama example (because the short form isn’t valid for local_model):
Pin the model tag explicitly — must be a tag your Ollama instance has pulled (ollama pull gemma2:9b). Bare "local_model": "ollama" errors out at bootstrap because the model name is too important to silently default.

Custom form — adopter-written impl

References an adopter-written class implementing the relevant contract. module is the path to the JS file; export is the named export (defaults to default); config is passed to the constructor. The same shape works for any substrate slot:
Limitation: sync bootstrap() can’t dynamic-import. Custom-via-connectors.json surfaces a clear error and falls back to the default. Adopters wanting custom impls today write a programmatic bootstrap that calls registry.registerSkillStore("primary", new MySkillStore(...)) (or registry.registerAgentConnector(...)) directly — same pattern as the runtime’s reference bootstrap(). Async-bootstrap with dynamic-import support is planned.

Precedence

When multiple config sources speak:
  1. Programmatic opts (opts.skillStore / opts.dataStore / opts.localModel / opts.agentConnector passed to bootstrap()) — explicit, highest priority
  2. connectors.json substrate section — declarative, deployment-durable
  3. Built-in default — fallback (filesystem skill_store; conditional sqlite data_store; no local_model; NoOpAgentConnector)
If two configs disagree, the higher-priority one wins; lower-priority is ignored without error.

Which surfaces honor substrate config?

The four authoring CLI commands stay FS-pinned by design — they’re the filesystem-first authoring loop. Sqlite-backed skills are authored via the dashboard UI or the skill_write MCP tool, not these CLI commands.

Named MCP connector instances

Per-host MCP connector wiring. Each top-level key (other than substrate) defines a named connector referenced via $ <name> in skill source.
Each entry needs:
  • class — a class from the closed-set registry. Today: RemoteMcpConnector (stdio-bridged remote MCP). Adopters can register custom classes via registerConnectorClass() from their bootstrap.
  • config — passed to the class’s fromConfig() factory. Schema is class-specific.
  • allowed_tools (optional) — per-connector tool allowlist at the entry top-level (sibling to class / config, NOT inside config). undefined = allow all; [] = allow none; listed array = exactly those. Placing allowed_tools inside the config: block is a hard parse error — the loader refuses to load to prevent a silent allow-all bypass (a security control quietly doing nothing on misplacement is the worst-case failure mode).

RemoteMcpConnector config — stdio framing

RemoteMcpConnector speaks JSON-RPC over the spawned child’s stdio. Two framing conventions are supported via the framing config key:
  • "newline" — one JSON-RPC message per line, newline-delimited. This is what mcp-remote (the npm package) and most spec-compliant MCP stdio servers use. Recommended for almost all adopters.
  • "lsp" (legacy default) — Content-Length: N\r\n\r\n<body> per the LSP convention. Only set this if your specific MCP server explicitly uses LSP-style framing.
With the wrong framing, the connector hangs to init_timeout because the child can’t parse the request. Set framing explicitly in your connector config to avoid the silent hang — the init-timeout error message names framing as a likely cause, but the explicit setting is the durable fix.

Credential discipline

connectors.json is secret-bearing. The repo .gitignore excludes it by default; connectors.json.example (not real values) is committed as a template. For deployments, prefer ${VAR} env-var substitution over literals — commit the ${...} references; keep secrets in deployment environment. Skillscript warns at bootstrap if connectors.json lives in a git-tracked directory without a .gitignore entry.

${VAR} substitution

${NAME} resolves from process.env at load time. Missing env var → clear startup error (not silent empty string). The config.env block is itself resolved first, then merged into the substitution scope for the rest of the config — letting you compose values:
This matches the Claude Desktop mcp.json convention.

Inline comments

Underscore-prefixed top-level keys (_comment, _note_security, etc.) are ignored by the parser. Use them inline to document your config without external comments — the JSON spec doesn’t natively support comments, so the runtime treats _* keys as the convention.

Adopter-custom substrate impls

Write class FooSkillStore implements SkillStore { ... } (or DataStore, LocalModel). Wire it via either: (a) Programmatic bootstrap (recommended today) — for the common case (wire everything from $SKILLSCRIPT_HOME like the CLI does), call bootstrapFromEnv() and declare your substrate in connectors.json; it loads .env + config + connectors.json, resolves the env cascade, and returns a fully-wired { wired, server } (both unstarted). Reach for the raw-Registry assembly below only when hand-constructing a substrate connectors.json can’t express:
See docs/adopter-playbook.md §“Programmatic bootstrap path” for both — bootstrapFromEnv() (recommended) and the raw pattern. (b) connectors.json custom form (deferred to follow-up):
Currently surfaces an error and falls back to the default — sync bootstrap() can’t dynamic-import. Track the async-bootstrap promotion as future work.

Operational tips

Switching substrates without losing data

The substrate switch is a runtime wiring change, not a data migration. Switching from filesystem to sqlite doesn’t move your .skill.md files into the Sqlite db automatically — the dashboard will show an empty skill list because the new Sqlite db is fresh. To preserve your skills across a switch:
  1. Read each .skill.md file from ~/.skillscript/skills/ (or your skillsDir)
  2. Call skill_write MCP tool (or store.store(name, source) programmatically) to land them in the new substrate
A bundled migration tool isn’t shipped — different adopters want different things (rename normalization, metadata enrichment, dry-run safety).

Multi-instance posture

Running both a dev instance (filesystem) and an adopter instance (sqlite or custom) side by side is common. Use separate --port + --connectors paths:
See docs/adopter-playbook.md § “Two-instance posture” for the broader pattern.

Verifying which substrate is wired

After a config change + restart, verify via runtime_capabilities:
Output includes the wired SkillStore’s implementation field (FilesystemSkillStore or SqliteSkillStore).