The same query_db tool does very different things in different places. On your laptop it points at a throwaway SQLite file you can delete and rebuild. In staging it reads a test database shared with your team. In production it runs against the real database holding real customers. The code is identical — only the configuration differs — and that gap is where the worst incidents hide: a tool that worked fine in testing silently points at production, sends a real email from a dry run, or charges a real card from a sandbox.
The discipline is environment-scoped configuration: resolve every tool's settings for the environment it is running in, make the safe setting the default, and treat production capabilities as something you opt into, not something you fall back onto.
flowchart LR T["same tool<br/>query_db"] --> L["local<br/>sqlite throwaway"] T --> S["staging<br/>shared test db"] T --> P["production<br/>real db"] style L fill:#166534,color:#fff style S fill:#b45309,color:#fff style P fill:#b91c1c,color:#fff
Three tiers, one resolution
Most systems settle on three tiers. Local is the developer's machine: cheap fakes, no network, anything destructive is safe because there is nothing real to destroy. Staging mirrors production but serves no real users: shared test data, deterministic seeds, dangerous side effects still disabled. Production is the live system with real users and real consequences.
The same tool must resolve a different configuration in each. A db connection string, a retries count, a timeout — each can sensibly differ by tier, and the resolution is a layered merge: start from defaults every tier shares, then overlay the values one specific tier overrides. The effective config is the merged result.
const CONFIG = {
defaults: { timeout: 5000, retries: 1 },
envs: {
local: { db: 'sqlite://local.db', retries: 0 },
staging: { db: 'postgres://staging', retries: 2 },
production: { db: 'postgres://prod', retries: 3 }
}
};
function resolveConfig(env) {
return Object.assign({}, CONFIG.defaults, CONFIG.envs[env]);
}
console.log(resolveConfig('local').db);
console.log(resolveConfig('local').retries);
console.log(resolveConfig('production').timeout);
flowchart TD D["defaults<br/>timeout, retries"] --> M["merge"] E["env override<br/>db, retries"] --> M M --> R["effective config<br/>for this environment"] style M fill:#7c3aed,color:#fff style R fill:#166534,color:#fff
The production fallback trap
The merge has one famous failure mode. A naive resolver looks up the environment's value and, if the key is missing, falls back to a default — and too often that default is production. Type the environment name slightly wrong, ask for staigng instead of staging, and the resolver finds nothing under that key, shrugs, and hands back the production connection string. A typo now routes test traffic at the live database.
The fix is to fail closed on unknown environments. Keep an explicit set of the environments you actually support, and if the requested tier is not in that set, throw — do not guess. A configuration system should never quietly substitute production for something it did not recognise.
const DB = {
local: 'sqlite://local.db',
staging: 'postgres://staging',
production: 'postgres://prod'
};
const KNOWN = new Set(['local', 'staging', 'production']);
function dbFor(env) {
if (!KNOWN.has(env)) throw new Error('unknown env: ' + env);
return DB[env];
}
console.log(dbFor('local'));
console.log(dbFor('staging'));
Gating dangerous capabilities by tier
Configuration is not only connection strings — it is also which tools may run at all. A capability like charge_card, send_email, or drop_table carries real-world cost, so the safe rule is to disable it in every tier that is not production and enable it only where it can do real good. Local and staging get a forbidden list: even if a tool is wired up, the gate refuses to run it. Production, the one tier with real users, is the only place these capabilities switch on.
This inverts the usual default. Instead of enabling a tool everywhere and disabling it where it hurts, you disable it everywhere and enable it in the single tier that earns it — production. The asymmetry matches the asymmetry of harm.
flowchart TD C["charge_card tool"] --> G["which environment"] G --> L["local: disabled"] G --> S["staging: disabled"] G --> P["production: enabled"] style L fill:#b91c1c,color:#fff style S fill:#b91c1c,color:#fff style P fill:#166534,color:#fff
Secrets are not configuration
Connection strings, API keys, and tokens are secrets, and they follow a stricter rule than ordinary config: they never live in the repository, never appear in the resolved object that gets logged, and are injected at runtime from a secret store keyed by environment. The resolver fetches the name of a secret from config and the value from the store — so a config file can name DB_PASSWORD in every tier without ever holding the production password. Treating secrets as ordinary config is how credentials leak into version control and build logs; the environment scope that protects your database must protect its password too.
const FORBIDDEN = {
local: new Set(['send_email', 'charge_card', 'drop_table']),
staging: new Set(['charge_card', 'drop_table']),
production: new Set()
};
function findUnsafe(tools, env) {
const ban = FORBIDDEN[env];
return tools.filter(function (t) { return t.enabled && ban.has(t.id); }).map(function (t) { return t.id; });
}
console.log(findUnsafe([{ id: 'send_email', enabled: true }], 'local'));
console.log(findUnsafe([{ id: 'send_email', enabled: true }], 'production'));
console.log(findUnsafe([{ id: 'send_email', enabled: false }], 'local'));
You add a charge_card tool. What is the safe default for which environments it runs in?
A capability with real-world cost is off by default everywhere and enabled only in the one tier that earns it — production. Local and staging put it on a forbidden list; the gate refuses to run it there even if it is wired up.
What does this print (two lines)? This resolver falls back to production when the environment is unknown — exactly the danger the lesson warns about.
const DB = { local: 'sqlite://local.db', staging: 'postgres://staging', production: 'postgres://prod' };
function dbFor(env) { return DB[env] || DB.production; }
console.log(dbFor('staging'));
console.log(dbFor('qa'));
dbFor('staging') finds DB.staging directly.
dbFor('qa') finds nothing under 'qa', so the || fallback hands back DB.production.
Write resolveConfig(env) that returns the effective configuration for an environment: a fresh object that merges CONFIG.defaults with CONFIG.envs[env], where the environment's values win on conflict. (CONFIG is already defined for you.)
const CONFIG = {
defaults: { timeout: 5000, retries: 1 },
envs: {
local: { db: 'sqlite://local.db', retries: 0 },
staging: { db: 'postgres://staging', retries: 2 },
production: { db: 'postgres://prod', retries: 3 }
}
};
function resolveConfig(env) {
// merge defaults with the override for this env (env wins on conflict)
}
Object.assign takes a target then sources; later sources overwrite earlier keys.
Start with a fresh {} so CONFIG.defaults is not mutated, then overlay CONFIG.envs[env].
const CONFIG = {
defaults: { timeout: 5000, retries: 1 },
envs: {
local: { db: 'sqlite://local.db', retries: 0 },
staging: { db: 'postgres://staging', retries: 2 },
production: { db: 'postgres://prod', retries: 3 }
}
};
function resolveConfig(env) {
return Object.assign({}, CONFIG.defaults, CONFIG.envs[env]);
}
This dbFor falls back to PRODUCTION credentials for any environment it does not recognise — a typo like staigng silently routes test traffic at the live database. Fix it so an environment not in the known set throws instead. (DB is already defined.)
const DB = { local: 'sqlite://local.db', staging: 'postgres://staging', production: 'postgres://prod' };
function dbFor(env) {
return DB[env] || DB.production;
}
Keep a Set of the three known environments.
Before the lookup, throw if the requested env is not in that Set — otherwise return DB[env].
const DB = { local: 'sqlite://local.db', staging: 'postgres://staging', production: 'postgres://prod' };
const KNOWN = new Set(['local', 'staging', 'production']);
function dbFor(env) {
if (!KNOWN.has(env)) throw new Error('unknown env: ' + env);
return DB[env];
}
Write findUnsafe(tools, env) that returns the ids of tools that are enabled in env but whose id is on that environment's forbidden list. FORBIDDEN maps each environment to a Set of forbidden tool ids. Return only the ids (not the whole tool objects). (FORBIDDEN is already defined.)
const FORBIDDEN = {
local: new Set(['send_email', 'charge_card', 'drop_table']),
staging: new Set(['charge_card', 'drop_table']),
production: new Set()
};
function findUnsafe(tools, env) {
// ids of tools enabled in env but on this env's forbidden list
}
Read the forbidden Set for this environment with FORBIDDEN[env].
Keep a tool only if it is enabled AND its id is in the ban, then map down to just the ids.
const FORBIDDEN = {
local: new Set(['send_email', 'charge_card', 'drop_table']),
staging: new Set(['charge_card', 'drop_table']),
production: new Set()
};
function findUnsafe(tools, env) {
const ban = FORBIDDEN[env];
return tools.filter(function (t) { return t.enabled && ban.has(t.id); }).map(function (t) { return t.id; });
}
Recap
- The same tool resolves different configuration in each tier: local (cheap fakes), staging (shared test data), production (real users).
resolveConfig(env)is a layered merge — shared defaults overlaid with one tier's overrides, the override winning on conflict.- Fail closed on unknown environments. A typo'd tier must throw, never fall back to production credentials — silent production substitution is the most expensive default in this layer.
- Secrets are not config: they live in a runtime store keyed by environment, never in the repository or the logged config object.
- Gate dangerous capabilities by tier:
charge_cardand friends stay on a forbidden list outside production;findUnsafe(tools, env)audits that gate up front.
That closes the module: define a tool, dispatch it safely, discover the right one, bundle it into a skill, sandbox it, and configure it across every environment.