This guide focuses on AppConfig Feature Flags for production systems, with practical trade-offs for reliability, security, and cost.
Your coding agent has twelve tools. One of them — shell_exec — starts returning weird side effects after a model upgrade. You need that tool gone in under a minute, not after a Lambda redeploy, alias traffic shift, and a nervous CloudFormation change set. Environment variables and SSM Parameter Store “eventually” work; AppConfig feature flags give you versioned, validated, polled config with explicit fail-closed semantics when the service is unreachable. That is the difference between a kill switch and a hope.
⚡ TL;DR: Put tool allowlists,
maxDepth, and model IDs in AppConfig (feature flags or hosted freeform JSON). Poll on a short interval from the agent runtime; fail closed when AppConfig cannot be fetched. Pair with depth limits from Lambda Recursive Loop Protection, tool contracts in AI Coding Agent Tool Schemas, and blast-radius tags in IAM Condition Keys for Agent Runtimes. Never bake kill switches into a redeploy-only path.
AppConfig Feature Flags: production guidance
Lambda environment variables are deploy-time. Changing TOOLS_ENABLED=shell_exec:false means a new version, alias update, and cold starts. For multi-tenant coding agents you also need per-tenant overrides: disable deploy_prod for tenant A without touching tenant B. AppConfig supports:
- Feature flags (boolean / multivariate) with targeting rules
- Hosted configuration (freeform JSON/YAML) for richer tool policy documents
- Deployment strategies (canary / linear) so a bad flag does not flash-crash every agent at once
- Validators (JSON Schema / Lambda) so a typo cannot empty the allowlist
// ❌ Kill switch buried in Lambda env — requires redeploy
const ENABLED = (process.env.ENABLED_TOOLS ?? "")
.split(",")
.filter(Boolean);
export function isToolEnabled(name: string): boolean {
return ENABLED.includes(name); // stale until next deploy
}
✅ Treat runtime policy as data. The function code should ask AppConfig, not ship the policy in the zip.
Feature flags vs freeform hosted config
Use feature flags when the decision is a small set of toggles: tool.shell_exec, tool.git_push, agent.maxDepth, model.id. Use hosted freeform when you need a structured policy:
{
"tools": {
"shell_exec": { "enabled": false, "reason": "incident-2026-09-19" },
"read_file": { "enabled": true },
"apply_patch": { "enabled": true, "maxBytes": 65536 }
},
"agent": { "maxDepth": 2, "modelId": "anthropic.claude-sonnet-4-20250514-v1:0" },
"failClosed": true
}
Flags are easier to flip in the console; freeform is easier to review in PRs and validate with JSON Schema. Many teams run both: flags for emergency kills, freeform for the resting policy.
// ✅ AppConfig agent extension / GetLatestConfiguration pattern
import {
AppConfigDataClient,
StartConfigurationSessionCommand,
GetLatestConfigurationCommand,
} from "@aws-sdk/client-appconfigdata";
const client = new AppConfigDataClient({});
let token: string | undefined;
let cached: AgentPolicy | null = null;
let fetchedAt = 0;
const POLL_MS = 30_000; // ✅ short for kill switches; not 5 minutes
type AgentPolicy = {
tools: Record<string, { enabled: boolean; maxBytes?: number }>;
agent: { maxDepth: number; modelId: string };
failClosed: boolean;
};
async function refreshPolicy(): Promise<AgentPolicy | null> {
if (!token) {
const start = await client.send(
new StartConfigurationSessionCommand({
ApplicationIdentifier: process.env.APPCONFIG_APP!,
EnvironmentIdentifier: process.env.APPCONFIG_ENV!,
ConfigurationProfileIdentifier: process.env.APPCONFIG_PROFILE!,
RequiredMinimumPollIntervalInSeconds: 15,
})
);
token = start.InitialConfigurationToken!;
}
const out = await client.send(
new GetLatestConfigurationCommand({ ConfigurationToken: token })
);
token = out.NextPollConfigurationToken ?? token;
if (out.Configuration && out.Configuration.byteLength > 0) {
cached = JSON.parse(Buffer.from(out.Configuration).toString("utf8"));
fetchedAt = Date.now();
}
return cached;
}
export async function getPolicy(): Promise<AgentPolicy> {
const stale = Date.now() - fetchedAt > POLL_MS;
if (!cached || stale) {
try {
const p = await refreshPolicy();
if (p) return p;
} catch (err) {
// fall through to fail-closed
console.error("appconfig_unreachable", err);
}
}
if (cached) return cached;
// ❌ Do NOT default to "all tools on" when AppConfig is down
return {
tools: {},
agent: { maxDepth: 0, modelId: process.env.SAFE_MODEL_ID! },
failClosed: true,
};
}
Poll interval and Lambda lifecycle
For request-path agents (API Gateway / Function URL), poll on a module-level cache with a 15–45s TTL. Do not call AppConfig on every tool invoke — that adds latency and throttling risk. The AWS AppConfig Lambda extension is often cleaner: it exposes http://localhost:2772 and handles session tokens for you.
// ✅ Prefer the Lambda extension HTTP endpoint when available
export async function getPolicyViaExtension(): Promise<AgentPolicy> {
const app = process.env.AWS_APPCONFIG_APPLICATION!;
const env = process.env.AWS_APPCONFIG_ENVIRONMENT!;
const profile = process.env.AWS_APPCONFIG_CONFIGURATION_PROFILE!;
const url =
`http://localhost:2772/applications/${app}/environments/${env}` +
`/configurations/${profile}`;
try {
const res = await fetch(url, { signal: AbortSignal.timeout(800) });
if (!res.ok) throw new Error(`appconfig_http_${res.status}`);
return (await res.json()) as AgentPolicy;
} catch {
// fail closed — empty allowlist
return {
tools: {},
agent: { maxDepth: 0, modelId: process.env.SAFE_MODEL_ID! },
failClosed: true,
};
}
}
Wire the policy into the tool router before the model sees the catalog. If shell_exec is disabled, omit it from toolConfig entirely — do not list it and then refuse. Models retry refused tools; missing tools change the plan.
Fail-closed when AppConfig is unreachable
This is the part most tutorials skip. Three failure modes:
- Cold start + extension not ready — first invoke times out talking to localhost:2772
- Throttling / regional incident — GetLatestConfiguration errors
- Empty configuration — deploy wiped the profile
// ✅ Tool gate that fails closed
export async function assertToolAllowed(
toolName: string,
depth: number
): Promise<{ ok: true } | { ok: false; error: string }> {
const policy = await getPolicy();
if (policy.failClosed && Object.keys(policy.tools).length === 0) {
return { ok: false, error: "agent_policy_unavailable" };
}
const t = policy.tools[toolName];
if (!t?.enabled) {
return { ok: false, error: "tool_disabled_by_flag" };
}
if (depth > policy.agent.maxDepth) {
return { ok: false, error: "maxDepth_flag_exceeded" };
}
return { ok: true };
}
Contrast with fail-open: a network blip re-enables deploy_prod for ten minutes. For coding agents with write tools, that is unacceptable. Pair fail-closed with a CloudWatch alarm on AgentPolicyUnavailable (see CloudWatch EMF for LLM Cost for the EMF counter pattern).
Targeting rules and incident playbooks
AppConfig feature flags support targeting by user ID, time window, and custom attributes. Map tenantId / agentRole into the retrieval context so you can disable git_push for a single noisy tenant without a global outage. Document the playbook:
- Flip
tool.shell_exec→ off (or push freeform withenabled: false) - Wait one poll interval (≤ 45s) — verify EMF
tool_disabled_by_flag - Fix root cause; re-enable with a canary deployment strategy
- Never “fix” by redeploying an older Lambda that still has the tool hardcoded on
IAM for the agent role: appconfig:StartConfigurationSession + appconfig:GetLatestConfiguration on the specific application ARN — not *. Combine with the tag conditions from the IAM condition-keys post so only runtimes tagged AgentRuntime=true can read the profile.
Checklist
- [ ] Tool allowlist / maxDepth / modelId live in AppConfig, not only Lambda env
- [ ] Choose flags (toggles) vs freeform JSON (policy doc) deliberately — or both
- [ ] Poll ≤ 45s for kill-switch freshness; use Lambda extension when possible
- [ ] Fail closed on unreachable / empty config (empty tool map, maxDepth 0)
- [ ] Omit disabled tools from Bedrock
toolConfig— do not advertise then refuse - [ ] Alarm on policy-unavailable and on emergency flag flips
- [ ] IAM scoped to the AppConfig application; no
appconfig:*on* - [ ] Incident runbook: flip flag → wait poll → verify → canary re-enable
Kill switches that require a redeploy are not kill switches. AppConfig turns agent tool policy into versioned, fail-closed runtime data — so the next hallucinated shell_exec storm ends with a flag flip, not a Friday deploy.
Related: Lambda Warm Pools: Low-Latency Backends for Coding Agent Tools; Human-in-the-Loop Gates: Dual Control for Prod-Touching Agent Tools; Day 54: Specialist Tools per Agent.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- LLM context windows: Tokens, Context Windows, and Why Models Forget Mid-Task
- PostgreSQL Performance Tuning: The Configuration Changes That Actually Matter
- Java Virtual Threads vs Traditional Threads: What Nobody Tells You
- LLM evaluation harness: Eval Harness Day One
Newly added
- DynamoDB Streams: Session Expiry Hooks for Multi-Turn Coding Agents
- S3 Conditional Writes: Idempotent Agent Artifact Uploads With If-None-Match
- Bedrock ApplyGuardrail API: Pre/Post Filters for Tool I/O in Coding Agents
- Lambda Destinations: Route Failed Agent Tool Invokes Without Silent Drops
- AppConfig Feature Flags: Kill Switches for Agent Tools Without Redeploy
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.