~/.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:
| Default path | Resolves to |
|---|---|
| Connectors file | $SKILLSCRIPT_HOME/connectors.json |
| Config file | $SKILLSCRIPT_HOME/skillscript.config.json |
| Triggers file | $SKILLSCRIPT_HOME/triggers.json |
Skills directory (skillsDir) | $SKILLSCRIPT_HOME/skills/ |
| Sqlite skill store dbPath | $SKILLSCRIPT_HOME/skills/skills.db |
| Sqlite data store dbPath | $SKILLSCRIPT_HOME/data.db |
| Trace directory | $SKILLSCRIPT_HOME/traces/ |
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
| Env var | Effect | Cascade precedence |
|---|---|---|
SKILLSCRIPT_HOME | Config root (default ~/.skillscript) | shell-set only — read before .env loads |
SKILLSCRIPT_FORCE_ALWAYS_DRAFT=true | Force outside-MCP skill_write to Draft; closes the agent-self-approval path | env > config > default false |
SKILLSCRIPT_ENABLE_UNSAFE_SHELL=true | Permit shell(unsafe=true) ops (syntax-scope axis) | env > config > default false |
SKILLSCRIPT_SHELL_ALLOWLIST=curl,git,jq | Comma-separated list of binaries reachable via shell(...) ops (binary-scope axis). Default-deny when unset — run skillfile shell-audit to discover your corpus’s set. Honored on both CLI and programmatic-bootstrap() paths; explicit bootstrap({ shellAllowlist: [...] }) (including [] deny-all) wins over env. See adopter-playbook § “Programmatic bootstrap path” for the precedence table. | bootstrap() opt > env > config > default-deny |
SKILLSCRIPT_FS_ALLOWLIST=/srv/work,/var/events | Comma-separated roots under which file_read / file_write may operate (path-scope axis). Default-deny when unset — every file op refused. Canonicalized before the check, so .. / symlink escapes are closed. Keep secret / key directories OUT. | env > config > default-deny |
SKILLSCRIPT_SECURED_MODE=true | Enforce the approval boundary: only Ed25519-signed skills perform effectful ops; unapproved or tampered skills are refused regardless of dispatch path (CLI / cron / /event / MCP / composition). Default false — a bare # Status: Approved is sufficient (unkeyed). See adopter-playbook § “Approval + secured mode”. | bootstrap() opt > env > config > default false |
SKILLSCRIPT_APPROVAL_KEY_FILE=<path> | Operator private signing key — read only by the approve flow, never on the execution hot path. Default ~/.config/skillscript/approval.key (deliberately outside SKILLSCRIPT_HOME). Auto-provisioned 0600 on first secured-mode start if absent. | env > default |
SKILLSCRIPT_APPROVAL_PUBLIC_KEY_FILE=<path> | Operator public verification key (non-secret) — read by the runtime to verify signatures on every execution. Default ~/.config/skillscript/approval.pub. | env > default |
SKILLSCRIPT_APPROVAL_PASSCODE=<passcode> | Opt into in-browser dashboard approval: when set, the dashboard signs server-side after a one-time passcode unlock (session-scoped, ~15-min idle TTL). Unset = dashboard is review-only; signing only via skillfile approve at a terminal. | env > default unset |
SKILLSCRIPT_DASHBOARD_AUTH_TOKEN=<token> | Gate the dashboard SPA + /rpc behind a token (?token= / cookie / Authorization: Bearer). Network hygiene for a dashboard reachable beyond localhost — not a forgery boundary. (/event keeps its own bearer gate.) | env > default unset |
SKILLSCRIPT_PORT=8080 | Dashboard / serve HTTP port | --port flag > env > config > default 7878 |
SKILLSCRIPT_HOST=0.0.0.0 | Bind address | --host flag > env > config > default 127.0.0.1 |
SKILLSCRIPT_MCP_CALLER_IDENTITY_HEADER=X-Agent-Id | Inbound caller-identity header name (multi-agent MCP hosts only — see adopter playbook) | env > config > default unset |
SKILLSCRIPT_POLL_INTERVAL_SECONDS=30 | Scheduler tick / poll interval (seconds) | env > config > default 30 |
SKILLSCRIPT_ABSOLUTE_TIMEOUT_MS=300000 | Runtime fallback timeout (ms) when no per-op / skill / connector default applies | env > config > default 300000 (5 min) |
SKILLSCRIPT_MAX_RECURSION_DEPTH=10 | Composition recursion depth ceiling for $ execute_skill chains | env > config > default 10 |
SKILLSCRIPT_EVENT_INGRESS_ENABLED=true | Mount POST /event for event-triggered skills. Default false — route returns 404 when not enabled. Shares SKILLSCRIPT_PORT with dashboard/RPC (one HTTP server). | env > default false |
SKILLSCRIPT_EVENT_INGRESS_AUTH_TOKEN=<token> | Bearer-token auth for POST /event. When set, every event POST requires Authorization: Bearer <token>; 401 otherwise. Default unset = open-internally (still gated by bind address). | env > default unset |
OLLAMA_BASE_URL=http://... | Ollama endpoint for LocalModel (default http://localhost:11434) | env > built-in default |
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:AMP_ENDPOINTandAMP_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:
| Slot | Values |
|---|---|
skill_store | "filesystem" | "sqlite" |
data_store | "sqlite" |
local_model | (none — "ollama" requires the object form with defaultModelTag; see below) |
agent_connector | "noop" (explicit silent fallback; same behavior as null) |
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:
| Type | Config fields |
|---|---|
filesystem (skill_store) | none — uses the CLI’s skillsDir (defaults to $SKILLSCRIPT_HOME/skills/) |
sqlite (skill_store) | dbPath (default: $SKILLSCRIPT_HOME/skills/skills.db) |
sqlite (data_store) | dbPath (default: $SKILLSCRIPT_HOME/data.db; DATA_DB env overrides) |
ollama (local_model) | baseUrl (default: OLLAMA_BASE_URL env or http://localhost:11434), defaultModelTag (required — e.g., "gemma2:9b", "llama3.1:8b") |
noop (agent_connector) | none — silent fallback (warn + resolve). Real adopter impls use the custom form below. |
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?
| Surface | Honors substrate? | Reasoning |
|---|---|---|
MCP server (skillfile serve, dashboard /rpc) | ✓ | MCP is the agent-facing surface; must read/write whichever store the deployment chose |
Web dashboard (skillfile dashboard) | ✓ | Same as MCP — agents and humans connect to the same runtime |
| Programmatic embed (your own bootstrap) | ✓ | You pass opts.skillStore directly; the runtime takes whatever |
skillfile compile | ✗ filesystem-only | Authoring loop: vim foo.skill.md && skillfile compile foo. Only coherent against FS. |
skillfile lint | ✗ filesystem-only | Same as compile. |
skillfile audit | ✗ filesystem-only | Operates on a provenance file + the FS-authored source. |
skillfile list | ✗ filesystem-only | Filesystem listing of .skill.md files. |
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).