name: aws-sst-development
description: SST v4 (Ion) expert for managing AWS resources as code with the Pulumi-backed framework. Use when writing or editing sst.config.ts, building infra/ modules (sst.aws.Function/Bucket/Dynamo/Cron/Service/Router, sst.Secret, sst.Linkable, raw aws.* Pulumi resources), wiring resource links, scoping IAM, or running sst deploy/dev/diff/remove. Essential when the user mentions SST, sst.config.ts, $config, $transform, $interpolate, sst.aws.*, sst.Secret, Pulumi/Ion, "sst deploy", a failed SST deploy (ConflictException on a resource-type change, "Identifier '__filename' has already been declared", MalformedPolicyDocument on an Output
- aws-mcp-setup allowed-tools:
- mcp__awsdocs__*
- mcp__aws-mcp__*
- Read
- Write
- Edit
- Glob
- Grep
- Bash(npx sst *)
- Bash(npm *)
- Bash(pnpm *)
- Bash(npx vitest *)
- Bash(aws sts get-caller-identity)
- Bash(aws ssm get-parameter*)
- Bash(aws ssm get-parameters-by-path*)
- Bash(aws lambda get-function*)
- Bash(aws lambda list-versions-by-function*)
hooks:
PreToolUse:
- matcher: Bash(npx sst deploy*) command: aws sts get-caller-identity --query Account --output text once: true
SST v4 for AWS
SST v4 (the "Ion" engine) is a Pulumi-backed IaC framework: you describe AWS
resources in TypeScript and SST/Pulumi reconciles them into your account. It
gives you high-level sst.aws.* components (Function, Bucket, Dynamo, Cron,
Service, …) that expand into many underlying resources, plus an escape hatch to
any raw Pulumi aws.* resource for the long tail. This skill encodes a
production-proven way to author, link, test, deploy, and troubleshoot SST
stacks on AWS — distilled from real multi-stack projects that have paid for
each lesson with a prod incident.
SST and Pulumi are third-party — verify current syntax with Context7
(resolve-library-id → query-docs for sst or pulumi-aws) when you're
unsure about a component's options. Verify AWS-side facts (service limits,
model IDs, IAM action names, region availability) with the AWS docs MCP, never
from memory. The patterns here are the how; the docs are the what.
When you're invoked
Figure out which mode you're in and jump to the right reference:
| Situation | Go to |
|---|---|
| New project, or adding a resource/module to an existing SST app | Author → references/authoring.md |
| Wiring one module's output into another (links, SSM, IAM scope) | Author → references/authoring.md § Sharing |
| Writing tests for infra so changes don't silently break | Test → references/testing.md |
| Running a deploy, or a deploy just failed | Deploy/Operate → references/deploy-and-troubleshoot.md |
| Migrating a resource between Pulumi types, renaming a physical name | Deploy/Operate → references/deploy-and-troubleshoot.md § Migrations |
Always read the relevant reference before editing — they carry the why behind each rule, which matters more than the rule itself.
Orientation: read the repo before you touch it
SST projects are conventional but not identical. Before editing, build a quick map so your change matches the house style instead of fighting it:
sst.config.ts— the app name,home, providers/region,defaultTags, any global$transform(Node runtime pin, bundle fixups), and the order in whichrun()importsinfra/modules. The import order is the dependency order; respect it.infra/— one file per domain (storage, functions, api, observability…). This is where resources are declared. Check for aninfra/CLAUDE.md— these projects keep IaC-specific rules there, and it's the single most valuable file to read first.infra/tests/— source-level Vitest assertions that pin resource invariants. If they exist, your change must keep them green and probably needs a new assertion.package.json/.nvmrc— package manager (npm vs pnpm), Node version, and thesst/pulumiversions actually installed.
Run npx sst version to confirm you're on v4/Ion (the $config + .sst/platform/
signature). v2/v3 ("SST Classic", CDK-based) is a different framework — these
patterns don't apply there.
The conventions, and which are universal vs tunable
The projects this skill is built from share a deliberate house style. Some of it is universal (true for any SST v4 + AWS project — apply it everywhere); some is project-specific (a sensible default these projects chose — adopt it for consistency, but recognize a project may differ).
Universal — these principles hold for any SST v4 + AWS project:
- Control the Node runtime deliberately, in one place. Don't leave it to
whatever the installed SST happens to default to. The idiom is a single global
$transform(sst.aws.Function, (args) => { args.runtime ??= "nodejs24.x" })inrun()—??=is correct here (the transform runs before the component applies its own default, so it fills in only when the user didn't set one). Recent SST already defaults to a current Node runtime, so check the installed default first (Context7); the transform is then version-independence insurance so a future SST downgrade can't silently move your fleet. Seereferences/authoring.md. - Never interpolate a Pulumi
Output<T>into a plain JS template literal. Use$interpolate(orpulumi.interpolate). A bare top-level`${bucket.arn}/*`stringifies theOutputto a[Output<T>]placeholder and produces a broken ARN that only fails at deploy time (it type-checks andsst devruns fine). The fix is$interpolate`${bucket.arn}/*`. This has caused prod deploy outages. Seereferences/authoring.md§ Outputs. - Migrating a resource between Pulumi types should default to two PRs —
Pulumi creates-before-destroys, so for a uniqueness-constrained AWS name
(bucket, IAM role, gateway) the old resource still owns it and the create
fails with
ConflictException. Two sequential deploys (teardown, then recreate) is the conservative default;aliases:/pulumi import/ state surgery can bridge identity in some cases but only with a reviewed plan. Seereferences/deploy-and-troubleshoot.md§ Migrations. - Prefer typed
sst.aws.*/aws.*resources over theaws.cloudcontrol.Resourceescape hatch. CloudControl outputs are stringly-typed andoneOffields don't patch cleanly. Use it only when no typed resource exists yet, and migrate off it when one ships.
Project-specific defaults — adopt for consistency, but confirm per repo:
- Region
ap-northeast-1,home: "aws", anddefaultTagscarryingProject/Stage/ManagedBy: "sst". - Stage-gated lifecycle:
removal: stage === "prod" ? "retain" : "remove"andprotect: stage === "prod"so prod resources survive a stack tear-down and non-prod previews clean up. - SSM Parameter Store as the out-of-graph contract under a
/{app}/{stage}/{domain}/...prefix — for consumers that aren't in the Pulumi graph (CI scripts, sibling apps, operators). For same-app Lambdas, prefer SSTlink:(it wires a real dependency edge and grants IAM); don't route same-app sharing through SSM. Seereferences/authoring.md§ Sharing. - Lazy
await import("./infra/<module>")insiderun()sosst devhot-reload stays light. (For testing, a module export still runs its top-levelnew sst.aws.*unless it's wrapped in a factory function — seereferences/testing.mdfor how to test infra.) - Source-level Vitest tests on every infra module — a lightweight,
house-style regression net asserting on the source text (resource names,
index shapes, IAM scopes). It's a deliberate choice, not an SST limit: Pulumi
does support runtime mocks (
@pulumi/pulumi/runtime) for behavioral graph tests when a module has real logic. Source assertions don't replace a preview-deploy + smoke test. Seereferences/testing.md. - An observability gate: every new Lambda/queue/schedule gets an alarm and
structured logging before merge. Whether you enforce this depends on the
project, but it's cheap insurance. See
references/deploy-and-troubleshoot.md§ Observability.
When you introduce a convention, say which bucket it's in ("this is universal" vs "matching this repo's house style") so the user can override the project-specific ones deliberately.
Working rhythm
- Orient (above) — map config, modules, tests, tooling.
- Verify syntax with Context7 / AWS docs MCP if anything is non-obvious. Don't guess at a component's option name.
- Author the resource/module following
references/authoring.md. Match the surrounding file's commenting density and naming — these projects comment the why heavily, and a terse one-liner in a heavily-annotated file reads as a regression. - Test — add or update source-level assertions (
references/testing.md) and runnpx vitest(or the repo'stestscript). Runnpx sst diffand/ortsc --noEmitto catch type and plan errors before deploying. - Deploy/operate per
references/deploy-and-troubleshoot.md. Confirm the target account withaws sts get-caller-identitybefore anysst deploy. - Clean up any exported state files — they contain account IDs and ARNs and
must not linger in
/tmpor chat history.
What good looks like
- The change is the smallest diff that satisfies the requirement, in the right
infra/module, wired intorun()in dependency order. - Every Lambda gets the right runtime via the global transform (you didn't
hand-set
runtimeunless intentionally diverging — e.g. a Python function). - Cross-resource references use
link:(in-graph) and/or$interpolate-scoped IAM; outputs other tools consume are published to SSM under the stage prefix. - New infra has a matching source-level test, and the existing suite stays green.
- You confirmed AWS-side facts via the docs MCP and SST/Pulumi syntax via Context7 rather than relying on recall.
- Anything irreversible (deploy,
sst remove, a resource-type migration) was flagged to the user with the account it targets, and migrations were planned as two PRs, not one.