Your coding agent’s system prompt lives in SYSTEM_PROMPT on three Lambda aliases, a Step Functions Map state, and a sticky note in Notion. Last week someone “temporarily” made the agent more aggressive about deleting files; nobody can prove which deploy did it. Amazon Bedrock Prompt Management is the unfair advantage: versioned prompts with aliases, A/B routing, and CloudTrail-friendly changes — wire them into Converse instead of baking prose into env vars. Pair with Bedrock Converse toolConfig, ApplyGuardrail, and AppConfig kill switches — this post is the persona/versioning layer.
⚡ TL;DR: Store system (and reusable user) templates in Bedrock Prompt Management. Pin agents to an alias (
prod,canary), not a raw version. Fetch by ARN at runtime (or use Prompt Management APIs with Converse). Audit changes; roll back by moving the alias. Related: Converse toolConfig, Prompt Flows, ApplyGuardrail.
Why env-var prompts rot
| Pattern | Versioned? | Audit who changed? | A/B? | Hot rollback? |
|---|---|---|---|---|
Lambda env SYSTEM_PROMPT |
❌ Deploy-tied | Weak (who pushed?) | ❌ | Redeploy |
S3 object prompt.txt |
Soft | Bucket logs | DIY | Overwrite |
| AppConfig freeform | ✅ | Partial | ✅ | ✅ |
| Bedrock Prompt Management | ✅ First-class | CloudTrail + console | ✅ Aliases | ✅ Move alias |
AppConfig is still excellent for kill switches and numeric knobs. Prompt Management is better when the artifact is a Bedrock-native prompt you want to compose with models and variables.
// ❌ Persona buried in infra
new lambda.Function(this, "Gateway", {
environment: {
SYSTEM_PROMPT: "You are a ruthless coding agent. Delete freely...", // nightmare
},
});
// ✅ Runtime resolves alias → immutable version text
const PROMPT_ALIAS_ARN =
"arn:aws:bedrock:us-east-1:123456789012:prompt/ABCDE:prod";
Create versions and aliases
Conceptually: Prompt resource → numbered versions → named aliases pointing at a version (like Lambda).
import boto3
br = boto3.client("bedrock-agent")
# Create prompt (API names vary slightly by SDK version — use current CreatePrompt)
prompt = br.create_prompt(
name="coding-agent-system",
description="System persona for multi-turn coding agents",
variants=[
{
"name": "default",
"templateType": "TEXT",
"templateConfiguration": {
"text": {
"text": (
"You are CheatCoders Agent for tenant {{tenant_id}}. "
"Prefer minimal diffs. Never run destructive git commands "
"unless tool policy allows. Current repo: {{repo_name}}."
),
"inputVariables": [
{"name": "tenant_id"},
{"name": "repo_name"},
],
}
},
"modelId": "anthropic.claude-sonnet-4-20250514-v1:0",
}
],
)
# Create a version, then alias "prod" → that version (via CreatePromptVersion / CreatePromptAlias)
// ✅ Get prompt content at runtime (bedrock-agent-runtime or agent GetPrompt)
import {
BedrockAgentClient,
GetPromptCommand,
} from "@aws-sdk/client-bedrock-agent";
const agent = new BedrockAgentClient({});
export async function loadSystemPrompt(aliasArn: string) {
// Resolve alias ARN or promptId + version
const out = await agent.send(
new GetPromptCommand({
promptIdentifier: aliasArn, // prompt id or ARN per API
})
);
// Extract text template from variants[0]
return out;
}
Document exact API field names against your SDK — the product moves quickly; the pattern (version + alias + variables) is what matters.
Wire to Converse (not env vars)
import {
BedrockRuntimeClient,
ConverseCommand,
} from "@aws-sdk/client-bedrock-runtime";
const runtime = new BedrockRuntimeClient({});
export async function planTurn(opts: {
tenantId: string;
repoName: string;
messages: { role: "user" | "assistant"; content: { text: string }[] }[];
tools: any[];
}) {
const systemText = await renderPromptAlias("coding-agent-system", "prod", {
tenant_id: opts.tenantId,
repo_name: opts.repoName,
});
// ✅ Optional: ApplyGuardrail on the rendered system + user turns
const resp = await runtime.send(
new ConverseCommand({
modelId: "anthropic.claude-sonnet-4-20250514-v1:0",
system: [{ text: systemText }],
messages: opts.messages,
toolConfig: { tools: opts.tools },
})
);
return resp;
}
# ❌ Concatenating unaudited fragments from DynamoDB + env + hardcode
system = os.environ["SYSTEM_PROMPT"] + extra_from_ddb + "DEBUG MODE ON"
Use variables for tenant/repo/policy snippets, not for untrusted user text (that belongs in messages). Keep idempotent tool results in toolConfig separate from the persona prompt.
A/B via aliases and audit who changed the persona
- Publish version 12 with a safer “ask before delete” persona
- Point alias
canary→ 12, keepprod→ 11 - Route 5% of tenants (or employees) to
canaryvia AppConfig percentage flag - Compare tool-deny rates, user thumbs-down, and Cost Anomaly token burn
- Move
prod→ 12 or rollcanaryback
// ✅ Alias selection from AppConfig — prompt body still in Prompt Management
const alias = flags.prompt_alias === "canary" ? "canary" : "prod";
const systemText = await renderPromptAlias("coding-agent-system", alias, vars);
CloudTrail (or Bedrock audit events) should show CreatePromptVersion / alias updates — pair with CloudTrail Lake style queries for “who changed agent persona last 7 days.”
For multi-step pipelines, Prompt Flows can reference managed prompts as nodes — still prefer aliases over hard-coded version IDs in the flow definition.
Guardrails and failure modes
- Missing variable: fail closed; do not call Converse with a half-rendered template
- Alias deleted: cache last-known-good in memory with short TTL + alarm
- Prompt injection in variables: only pass server-side tenant metadata into template variables
- Length: huge system prompts cost tokens every turn — keep policy details in tools + Verified Permissions, not a 8k-token sermon
// ✅ Fail closed on render errors
function render(template: string, vars: Record<string, string>) {
return template.replace(/\{\{(\w+)\}\}/g, (_, k) => {
if (!(k in vars)) throw new Error(`missing prompt var ${k}`); // ✅
return vars[k];
});
}
// ❌ Silent empty substitution — agent runs with blank tenant policy
template.replace(/\{\{(\w+)\}\}/g, (_, k) => vars[k] ?? "");
Multi-tenant personas without prompt sprawl
You do not need one Prompt Management resource per tenant. Prefer:
- One shared system prompt with variables
{{tenant_id}},{{policy_tier}},{{repo_name}} - Tier aliases (
prod-strict,prod-standard) for different product SKUs - Tenant overlays in DynamoDB (short policy bullets) merged after the managed template — still audited, but not 10k Bedrock prompt objects
# ✅ Compose managed prompt + small tenant overlay
base = render_prompt_alias("coding-agent-system", "prod", {
"tenant_id": tenant,
"repo_name": repo,
"policy_tier": tier,
})
overlay = ddb.get_item(...).get("persona_overlay", "")
if len(overlay) > 2000:
raise ValueError("overlay too large") # ✅ bound cost + injection surface
system = base + "\n\nTenant policy:\n" + overlay
// ❌ Forking entire prompt text per tenant in Prompt Management
// operational nightmare — prefer variables + thin overlays
When an overlay and the managed prompt conflict (“never delete” vs overlay “delete freely”), fail closed or have Verified Permissions refuse destructive tools regardless of prose. Prompts steer models; Cedar and SCPs enforce reality.
Track prompt version IDs in your session metadata (MemoryDB/DynamoDB) so support can answer “which persona ran when the agent wiped the branch?”
Checklist
- [ ] Move system prompts out of Lambda env vars into Bedrock Prompt Management
- [ ] Agents pin aliases (
prod/canary), never mutable draft text - [ ] Render variables server-side; never feed raw user text into template slots
- [ ] A/B with alias + AppConfig; roll back by moving alias
- [ ] ApplyGuardrail on rendered I/O; Verified Permissions on tools
- [ ] Alarm on GetPrompt/alias failures; cache short TTL last-known-good only with alerts
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Distributed Locks Reality Check: When Redis Redlock Is the Wrong Tool
- Ai Differential Testing: AI Oracles Validated Against Shadow Traffic
- Spec-First AI Development: OpenAPI Remains the Only Source of Truth
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Newly added
- AWS Organizations SCPs: Hard Caps on What Coding-Agent Accounts Can Call
- Amazon Bedrock Prompt Management: Versioned System Prompts for Coding Agents
- AWS CodeArtifact: Private Package Mirrors Inside Coding-Agent Sandboxes
- Amazon MemoryDB: Durable Sub-Millisecond Session State for Multi-Turn Coding Agents
- AWS Lambda SnapStart: Cut Coding-Agent Cold Starts Without Always-On Waste
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.