~/.skillscript/connectors.json (or any path passed via --connectors). It has two top-level concerns:
substrate— whichSkillStore,DataStore,LocalModel, andAgentConnectorthe runtime hosts (MCP server + web dashboard) use.- Named MCP connector instances —
youtrack,github, etc. — invoked via$ <name>in skill source.
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:
~/.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. WithoutSKILLSCRIPT_HOMEisolation, two daemons running side-by-side would sharetriggers.json,skillsDir(filesystem default), and any other$HOME/<thing>default — even if their sqlitedbPaths were explicitly distinct.SKILLSCRIPT_HOMEis 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-processenvblocks. Example:MEMORY_ENDPOINTandMEMORY_TOKENin.env; declarative shape committed toconnectors.json.
.env file format
Standard dotenv conventions:
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)
- CLI flag (e.g.,
--port 8080) - Shell-set env var (
export SKILLSCRIPT_PORT=8080) .envfile in$SKILLSCRIPT_HOMEskillscript.config.jsonfield- 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.jsonwith${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:
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:
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-innode:sqlitemodule, 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 withNODE_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
~/.skillscript/).
Valid short-form values per slot:
Null — explicit “no substrate”
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:
Worked Ollama example (because the short form isn’t valid forSqliteDataStorefeature surface. The bundledsqlitedata_store is a deliberately minimal reference implementation:supports_writes+supports_tag_filterare true;supports_semantic,supports_pinning,supports_decay_model,supports_thread_status_filterare all false. Rich features (semantic retrieval, pinning, decay scoring, thread-status workflow) come from substrate impls — adopters forkexamples/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.
local_model):
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
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: syncbootstrap()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 callsregistry.registerSkillStore("primary", new MySkillStore(...))(orregistry.registerAgentConnector(...)) directly — same pattern as the runtime’s referencebootstrap(). Async-bootstrap with dynamic-import support is planned.
Precedence
When multiple config sources speak:- Programmatic opts (
opts.skillStore/opts.dataStore/opts.localModel/opts.agentConnectorpassed tobootstrap()) — explicit, highest priority connectors.jsonsubstrate section — declarative, deployment-durable- Built-in default — fallback (filesystem skill_store; conditional sqlite data_store; no local_model; NoOpAgentConnector)
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 thansubstrate) defines a named connector referenced via $ <name> in skill source.
class— a class from the closed-set registry. Today:RemoteMcpConnector(stdio-bridged remote MCP). Adopters can register custom classes viaregisterConnectorClass()from their bootstrap.config— passed to the class’sfromConfig()factory. Schema is class-specific.allowed_tools(optional) — per-connector tool allowlist at the entry top-level (sibling toclass/config, NOT insideconfig).undefined= allow all;[]= allow none; listed array = exactly those. Placingallowed_toolsinside theconfig: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 whatmcp-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.
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:
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
Writeclass 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:
docs/adopter-playbook.md §“Programmatic bootstrap path” for both — bootstrapFromEnv() (recommended) and the raw pattern.
(b) connectors.json custom form (deferred to follow-up):
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 fromfilesystem 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:
- Read each
.skill.mdfile from~/.skillscript/skills/(or yourskillsDir) - Call
skill_writeMCP tool (orstore.store(name, source)programmatically) to land them in the new substrate
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:
docs/adopter-playbook.md § “Two-instance posture” for the broader pattern.
Verifying which substrate is wired
After a config change + restart, verify viaruntime_capabilities:
implementation field (FilesystemSkillStore or SqliteSkillStore).