Configuration
Running sfdt init creates a .sfdt/ directory in your project root. Every command reads it.
The .sfdt/ directory
- config.json
- environments.json
- test-config.json
- pull-config.json
- *.local.json
- feature-flags.json
- telemetry-snapshot.json
- bridge-token
| File | What it holds |
|---|---|
config.json | Core settings: org aliases, feature flags, AI provider, coverage threshold (see below) |
environments.json | Named environments and org aliases |
test-config.json | Test classes, coverage threshold, test level, and parallelDelay (seconds between parallel test batches, default 1 → SFDT_PARALLEL_DELAY) |
pull-config.json | Metadata types to pull from the org (see Incremental Pull) |
*.local.json | Optional environment-specific overrides — git-ignore these |
plugins/ | Optional local CLI plugins (.js/.mjs) — see Plugins |
cache/ | Pull delta cache (SQLite) and parked MCP results |
data/ | Data-set definitions for sf data tree |
feature-flags.json | Kill-switch list for Chrome extension features |
telemetry-snapshot.json | Latest opt-in extension telemetry snapshot |
bridge-token | Auto-created 0600 bearer token for the local bridge |
Add .sfdt/*.local.json to your .gitignore so machine-specific overrides never get
committed.
config.json reference
A representative config.json:
{
"projectName": "My Salesforce Project",
"defaultOrg": "my-dev-org",
"deployment": {
"coverageThreshold": 75,
"backupBeforeRollback": true,
"preflight": {
"enforceTests": false,
"enforceBranchNaming": false,
"enforceChangelog": false
}
},
"features": {
"ai": true,
"notifications": false,
"releaseManagement": true
},
"ai": { "provider": "claude", "model": "" },
"pullCache": { "enabled": true, "parallelism": 5, "batchSize": 100 },
"mcp": { "enabled": false, "parking": { "enabled": true } },
"plugins": []
}Top-level keys
| Key | Type | Description |
|---|---|---|
projectName | string | Display name used in logs and notifications |
defaultOrg | string | Default Salesforce org alias |
releaseNotesDir | string | Release-notes output directory (default release-notes) |
manifestDir | string | Manifest output directory (default manifest/release) |
manifestLayout | enum | flat (default) or subpath for per-package subdirectories |
changelogDir | string | Per-package changelog directory (default changelogs) |
logDir | string | CLI log directory (default logs) |
defaultBranch | string | Default base branch for deploy --create-pr / release PRs (default main; a user-exported SFDT_DEFAULT_BRANCH wins) |
smokeTests.testClasses | array | Apex test classes run by sfdt smoke (reaches the script via SFDT_SMOKE_TESTS; a user-exported env value wins) |
plugins | array | npm package names to load as plugins — requires SFDT_ALLOW_UNSAFE_CONFIG=1 since 0.22.0 (why) |
pluginOptions.autoDiscover | boolean | Auto-load sfdt-plugin-* packages and .sfdt/plugins/*.js (default false) — requires SFDT_ALLOW_UNSAFE_CONFIG=1 since 0.22.0 |
Repo-supplied settings that need an opt-in
.sfdt/config.json is meant to be committed — sfdt init recommends gitignoring only
.sfdt/*.local.json — so it arrives with whatever repository you cloned, written by whoever wrote
it. Since 0.22.0, the settings in it that execute code or choose where secrets are sent are
refused at load time. sfdt names each one it skipped and how to allow it; everything else in the
file loads normally.
| Setting | Why it is guarded |
|---|---|
plugins[] | import()ed at CLI startup, before any command runs |
pluginOptions.autoDiscover | Imports every sfdt-plugin-* in node_modules/ and every file in .sfdt/plugins/ |
mcp.salesforce.command / args | Spawned as a process |
ai.baseURL (non-loopback) | Chooses where prompts — and any secret named by ai.apiKeyEnv / ai.headersEnv — are sent |
A notification channel’s headersEnv beside a literal remote webhookUrl/url | Reads the named env vars and posts them to a URL fixed by the repo |
To allow them:
export SFDT_ALLOW_UNSAFE_CONFIG=1The opt-in is an environment variable, not a config key — by necessity. A flag inside
config.json would be set by the same person who set the guarded setting, which defeats the
purpose. Put the export in your shell profile or the CI job that needs it, never in the repo’s
own config.
Unaffected, no opt-in needed: ai.apiKeyEnv and ai.headersEnv (inert once the destination is
gone), a loopback ai.baseURL — so Ollama, LM Studio, llama.cpp and vLLM keep working — a
channel using webhookUrlEnv, an ordinary Slack webhookUrl with no headersEnv, and
defaultOrg.
deployment
| Key | Type | Default | Description |
|---|---|---|---|
deployment.coverageThreshold | number | 75 | Minimum Apex test coverage percent |
deployment.backupBeforeRollback | boolean | true | Capture an org snapshot before sfdt rollback |
deployment.keyClasses | array | [] | Key Apex class names for test-selection heuristics |
deployment.preflight.enforceTests | boolean | false | Promote “no tests” from warning to failure |
deployment.preflight.enforceBranchNaming | boolean | false | Enforce branch-naming convention |
deployment.preflight.enforceChangelog | boolean | false | Require an [Unreleased] changelog entry |
deployment.preflight.strict | boolean | false | Promote every preflight warning to a failure |
strict overrides the per-check flags. A check you left as a warning by setting its
enforceX: false is still promoted to a failure when strict: true. For the opt-in flags
above, false behaves the same as omitting the key — it leaves the check as a non-fatal
warning, it does not actively suppress it under strict mode.
These preflight flags are editable from the dashboard (Settings → Config) as of v0.15.1 —
the dashboard shows an inline safety caution when you change one. (defaultOrg, plugins, and
the MCP command settings remain file-only.)
features
Feature toggles. All default to true except notifications.
| Key | Description |
|---|---|
features.ai | AI-powered commands |
features.notifications | Slack integration |
features.releaseManagement | Release workflow |
features.monitoring | Org monitoring |
features.audit | Audit-trail analysis |
features.docs | Docs generation |
features.data | Data import/export |
features.scratch | Scratch-org management |
ai
| Key | Type | Description |
|---|---|---|
ai.provider | enum | claude (default), gemini, openai, or http (text-only endpoint) |
ai.model | string | Optional model override (the provider CLI handles selection) |
ai.baseURL | string | OpenAI-compatible endpoint URL (http provider only) |
ai.apiKeyEnv | string | Name of the env var holding the API key (http provider only) |
See AI Providers for full setup.
pullCache
| Key | Type | Default | Description |
|---|---|---|---|
pullCache.enabled | boolean | true | Enable the SQLite-backed incremental pull cache |
pullCache.parallelism | number | 5 | Concurrent retrieval threads |
pullCache.batchSize | number | 100 | Components per retrieve batch |
pullCache.retrieveTimeoutSeconds | number | 360 | Retrieve timeout |
mcp
| Key | Type | Default | Description |
|---|---|---|---|
mcp.enabled | boolean | false | Enable the MCP server |
mcp.parking.enabled | boolean | true | Park oversized tool results |
mcp.parking.thresholdBytes | number | 50000 | Size limit before a result is parked |
mcp.parking.ttlSeconds | number | 86400 | TTL before parked cache files are deleted |
mcp.parking.cacheScope | enum | session | SEP-2549 cache scope: global, user, or session |
See the MCP Server page for details.
audit, quality, monitoring, docs, data, scratch, soql
| Key | Default | Description |
|---|---|---|
audit.auditTrailLookbackDays | 30 | Setup-audit-trail lookback window |
audit.licenseWarnThreshold | 0.75 | License-usage warn threshold (0–1) |
audit.inactiveUserDays | 90 | Days before a user is “inactive” |
audit.minApiVersion | 45 | Minimum acceptable API version (the hard floor for audit api-versions and sfdt versions) |
audit.apiVersionWarnBehind | 0 | When > 0, also warn on components more than N versions behind the org’s max API version (0 = off); findings are tagged below-floor vs behind-ceiling |
audit.fieldDescriptionMaxMissing | 0 | Max custom fields allowed without a description before warning |
audit.connectedAppFlagPermissive | true | Flag connected apps that permit all users (admin approval off) |
audit.soapLoginLookbackDays | 30 | Lookback window for the soap-logins check (retiring SOAP login() traffic) |
monitoring.backupDir | monitoring-backup | Metadata-backup output directory |
monitoring.limitWarnThreshold | 0.75 | Governor-limit warn threshold (0–1) |
monitoring.errorLookbackDays | 7 | Apex-error lookback window |
monitoring.healthMinScore | 80 | Minimum acceptable Security Health Check score |
monitoring.orgInfoTrialWarnDays | 14 | Warn when a trial/expiration window is within this many days |
monitoring.deployHistoryLookback | 20 | Number of recent deployments inspected by deploy-history |
monitoring.deprecatedApiLookbackDays | 7 | Lookback window for deprecated-API traffic |
docs.outputDir | docs | Generated-docs output directory |
docs.ai | true | Include an AI overview in generated docs (requires features.ai) |
docs.diagrams | true | Emit the standalone ER-diagram page during docs generate |
docs.roleGuides | false | Emit per-component role guides during docs generate (requires features.ai) |
docs.roles | [] | Which role guides to emit (developer, admin, user, devops); empty = all four |
data.dir | .sfdt/data | Data-set definition directory |
scratch.definitionFile | config/project-scratch-def.json | Scratch-org definition |
scratch.durationDays | 7 | Scratch-org lifetime |
scratch.poolSize | 0 | Pre-created scratch-org pool size |
soql.defaultLimit | 200 | Default row bound for sfdt soql query/sosl when --limit isn’t passed |
soql.maxLimit | 2000 | Hard row cap for sfdt soql query/sosl — any requested or in-query LIMIT above it is clamped |
audit.licenseWarnThreshold and monitoring.limitWarnThreshold both default to 0.75 so the
CLI and the Chrome extension’s live org-health checks band
org-health usage identically (shared via @sfdt/flow-core).
ci
Defaults for sfdt ci init. Flags override
config; config overrides built-in defaults. (--type scratch also reads the existing
scratch.definitionFile key.)
{
"ci": {
"authMethod": "sfdx-url", // sfdx-url | jwt
"environment": "production", // release approval environment
"runner": "npx" // npx | docker | action
}
}notifications
Multi-channel push to Slack, Microsoft Teams, Google Chat, email, a generic webhook, or Grafana
Loki. Secrets are referenced by env-var name, never inline. The older single-Slack shape is
still honored when features.notifications: true.
{
"features": { "notifications": true },
"notifications": {
"enabled": false, // master switch for the channels[] system
"summary": { "enabled": false }, // send AI executive summaries for snapshots
"channels": [
// type: slack | teams | googlechat | email | webhook
// common: name, severityThreshold (ok|warn|error|fail), events[]
// slack/teams/googlechat: webhookUrl | webhookUrlEnv
// webhook: url | urlEnv, format (json|loki), headers{}, headersEnv{} (auth — env-var NAMES)
// email: from, to[], smtp{ hostEnv, portEnv, userEnv, passwordEnv, secure }
],
// legacy single-Slack shape (still honored when features.notifications=true):
"slack": { "webhookUrl": "", "webhookUrlEnv": "" }
}
}See the Notifications page for channel fields and severity routing.
deployment.smart
Settings for Smart Deploy (sfdt deploy --smart).
{
"deployment": {
"smart": {
"enabled": false,
"deltaBase": "main",
"noOverwriteManifest": "manifest/package-no-overwrite.xml",
"impactingTypes": ["ApexClass", "ApexTrigger", "Flow"],
"downgradeTestsOnNonProd": true,
"useRelevantTests": false,
"assumeProd": false
}
}
}deployment.smart.useRelevantTests (default false) opts non-production orgs on API ≥ 66 into
Salesforce’s beta RunRelevantTests in place of the RunLocalTests fallback — see
Smart Deploy.
ai.headers / ai.headersEnv
Extra request headers for the http provider. headers takes literal values; headersEnv
maps a header name to the name of an env var holding its value — use it for anything secret,
so the token stays out of .sfdt/config.json. headersEnv overrides a literal of the same name,
and an unset variable fails the request naming it rather than calling the endpoint without the
header. See AI Providers.
{ "ai": { "headersEnv": { "X-Org-Id": "GATEWAY_ORG_ID" } } }ai.agent
The bounded coding-agent auto-fix loop for sfdt deploy --smart --ai-fix. Off by default; requires
both enabled and allowWrite. See Smart Deploy.
{ "ai": { "agent": { "enabled": false, "maxTurns": 3, "allowWrite": false } } }Reading & writing config
Use dot notation from the command line:
sfdt config get defaultOrg
sfdt config get deployment.coverageThreshold
sfdt config set deployment.coverageThreshold 80
sfdt config set features.ai trueValues are coerced automatically: "true"/"false" become booleans, numeric strings become
numbers, everything else stays a string.