AWS Systems Manager Parameter Store: Hierarchical Config Coding Agents Must Not Hardcode

2 views

If your coding agent’s model id, max-tool-hops, and allowed repos live as Python constants, you will ship a “quick fix” that only exists on one host. AWS Systems Manager Parameter Store stores a hierarchical parameter tree — /prod/coding-agent/patcher/model_id — with SecureString encryption and IAM path scoping so each agent role reads only its slice. Distinct from Secrets Manager rotation (credential lifecycle + rotation Lambdas), AppConfig feature flags (flag/kill-switch deploy strategy), and Session Manager (break-glass shell): here we put non-secret and lightly sensitive config in a path hierarchy agents fetch at runtime.

⚡ TL;DR: Design /<env>/coding-agent/<role>/... paths, use String for knobs and SecureString for lightly sensitive values, IAM-scope ssm:GetParametersByPath, cache with TTL, and reserve Secrets Manager for rotating credentials. Related: Secrets Manager rotation, AppConfig kill switches, Session Manager shells, Budgets.

What goes in Parameter Store vs neighbors

Kind of value Store
Model id, temperature, max hops, allowlists Parameter Store String
API tokens, DB passwords, GitHub PATs Secrets Manager (rotation)
Kill switch / gradual flag rollout AppConfig
Human break-glass shell Session Manager
Prompt body versions Bedrock Prompt Management (runtime) + pin in SSM

❌ Putting the GitHub PAT in Parameter Store SecureString “because it’s encrypted” and never rotating it — use Secrets Manager.

Hierarchy that scales across envs and roles

/prod/coding-agent/shared/bedrock_region = us-east-1
/prod/coding-agent/shared/audit_bucket = coding-agent-audit-prod
/prod/coding-agent/patcher/model_id = anthropic.claude-sonnet-4-20250514-v1:0
/prod/coding-agent/patcher/max_tool_hops = 8
/prod/coding-agent/patcher/prompt_version = 3
/prod/coding-agent/patcher/repos_allowlist = acme/payments,acme/billing
/prod/coding-agent/reviewer/model_id = anthropic.claude-sonnet-4-20250514-v1:0
/prod/coding-agent/reviewer/strict_mode = true
/staging/coding-agent/patcher/model_id = anthropic.claude-haiku-...
bash
# ✅ put parameters (standard tier unless you need advanced throughput/patterns)
aws ssm put-parameter \
  --name /prod/coding-agent/patcher/model_id \
  --type String \
  --value "anthropic.claude-sonnet-4-20250514-v1:0" \
  --overwrite \
  --tags Key=workload,Value=coding-agent Key=env,Value=prod

aws ssm put-parameter \
  --name /prod/coding-agent/patcher/webhook_hmac \
  --type SecureString \
  --value "whsec_..." \
  --key-id alias/coding-agent-config \
  --overwrite

Use advanced tier when you need parameter policies, larger size, or higher throughput — not by default for every knob.

IAM path scoping (the whole point)

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadPatcherConfig",
      "Effect": "Allow",
      "Action": [
        "ssm:GetParameter",
        "ssm:GetParameters",
        "ssm:GetParametersByPath"
      ],
      "Resource": [
        "arn:aws:ssm:us-east-1:111122223333:parameter/prod/coding-agent/patcher/*",
        "arn:aws:ssm:us-east-1:111122223333:parameter/prod/coding-agent/shared/*"
      ]
    },
    {
      "Sid": "DecryptSecureString",
      "Effect": "Allow",
      "Action": ["kms:Decrypt"],
      "Resource": "arn:aws:kms:us-east-1:111122223333:key/your-key",
      "Condition": {
        "StringEquals": {
          "kms:ViaService": "ssm.us-east-1.amazonaws.com"
        }
      }
    },
    {
      "Sid": "DenyOtherEnvs",
      "Effect": "Deny",
      "Action": ["ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath"],
      "Resource": "arn:aws:ssm:us-east-1:111122223333:parameter/staging/*"
    }
  ]
}

Patcher role cannot read reviewer paths; staging cannot read prod. Pair with Access Analyzer so someone does not “temporarily” grant /prod/*.

Agent bootstrap: load by path, cache with TTL

python
# ✅ load hierarchy once per process; refresh on TTL or SIGHUP
import time, boto3
from typing import Any

ssm = boto3.client("ssm")

class AgentConfig:
    def __init__(self, path: str, ttl_sec: int = 60):
        self.path = path.rstrip("/")
        self.ttl = ttl_sec
        self._data: dict[str, Any] = {}
        self._loaded_at = 0.0

    def refresh(self) -> None:
        paginator = ssm.get_paginator("get_parameters_by_path")
        data = {}
        for page in paginator.paginate(Path=self.path, Recursive=True, WithDecryption=True):
            for p in page["Parameters"]:
                key = p["Name"][len(self.path) + 1 :]  # relative
                data[key] = p["Value"]
        self._data = data
        self._loaded_at = time.time()

    def get(self, key: str, default: str | None = None) -> str | None:
        if time.time() - self._loaded_at > self.ttl:
            self.refresh()
        return self._data.get(key, default)

cfg = AgentConfig("/prod/coding-agent/patcher")
model = cfg.get("model_id")
max_hops = int(cfg.get("max_tool_hops", "6"))
# ❌ MODEL_ID = "anthropic.claude-..." hardcoded in source

For instant kill switches (disable apply_patch globally), still prefer AppConfig with validated flag documents — Parameter Store is config; AppConfig is change strategy.

Change management without mystery meat

Practice Why
Infra-as-code for parameters PR review on config diffs
Tags env, workload, owner Cost + ownership
Separate paths per env No staging overwrite prod
CloudTrail on PutParameter Who changed max_tool_hops at 2am
Emit config version in logs Join Athena audits to knobs
bash
# ✅ detect drift: required keys present
required="model_id max_tool_hops prompt_version repos_allowlist"
for k in $required; do
  aws ssm get-parameter --name "/prod/coding-agent/patcher/$k" >/dev/null || echo "MISSING $k"
done

Wire prompt_version here and load the actual text from Bedrock Prompt Management — SSM stores the pin, not the essay.

Failure modes

Issue Symptom Mitigation
Throttle on GetParametersByPath Agent cold-start spikes Cache TTL; advanced tier if needed
SecureString without KMS grant Decrypt failures ViaService condition + key policy
Flat names /model_id Cross-role bleed Enforce hierarchy + IAM paths
Config in prompt text Model invents knobs Pass structured config to tools only
Dual writes SSM + hardcoded Silent divergence Fail boot if required path empty

Break-glass human access to the sandbox remains Session Manager — different SSM feature, same product family, do not conflate in runbooks.

Production checklist

  • [ ] Hierarchy /<env>/coding-agent/<role>/... documented
  • [ ] IAM path scope per role; Deny other envs
  • [ ] String vs SecureString vs Secrets Manager decision table
  • [ ] Client cache TTL; boot fails closed on missing required keys
  • [ ] IaC + CloudTrail for PutParameter
  • [ ] AppConfig for kill switches; SSM for durable knobs
  • [ ] Prompt version pin in SSM → Prompt Management asset
  • [ ] Tags and Budgets alignment
  • [ ] Runbook distinguishes Parameter Store / Secrets / AppConfig / Session Manager

FAQ

Q: Standard vs advanced parameters?
A: Start standard. Move to advanced for size >4KB, policies (expiration), or higher throughput. Don’t pay advanced for every boolean flag.

Q: Can Parameter Store replace AppConfig?
A: Not for gradual rollouts, validators, and deployment strategies. Many teams use both: SSM for baseline knobs, AppConfig for flags that must flip safely.

Q: How do agents in Batch/Fargate authenticate?
A: Task/job role with path-scoped ssm:Get*. No long-lived keys. Same pattern as other AWS APIs.

Related reading

Put knobs in a path. Scope the path. Stop hardcoding the agent’s brain in source control.

Last updated on October 4, 2026

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.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.