Your Lambda Passes Tests Then Throws AccessDeniedException in Production A developer built Infrawise, an open-source tool that prevents AWS AccessDeniedException errors by comparing a Lambda function's code dependencies against its IAM role permissions. The tool scans code for service edges like DynamoDB queries or SNS publishes, fetches the actual allowed services from the role's policies, and reports mismatches before deployment. You asked Claude Code to add a step to your order handler: pull the Stripe key out of Secrets Manager, then publish an order.settled event to SNS. It wrote clean code. Your unit tests mock both SDK clients, so they pass. CI is green. You deploy. Then the first real invocation logs this: AccessDeniedException: User: arn:aws:sts::123456789012:assumed-role/order-processor-role/order-processor is not authorized to perform: sns:Publish on resource: arn:aws:sns:us-east-1:123456789012:order-events Nothing was wrong with the code. The execution role was written months ago, when the function only touched DynamoDB. The code grew a new dependency and the policy did not follow. This is one of the most annoying classes of AWS bug because it is invisible to every tool in the loop. The type checker sees a valid PublishCommand . The linter sees valid TypeScript. The AI assistant reads your handler and has no idea what order-processor-role allows, because the policy lives in AWS or in a Terraform file three directories away , not in the file it is editing. Infrawise closes that gap by comparing the two directly. GitHub https://github.com/Sidd27/infrawise · npm https://www.npmjs.com/package/infrawise An AI coding assistant works from the files you give it. When it writes a Lambda handler, it can see: What it cannot see is the runtime identity that code will execute as. The link between "this handler" and "this IAM role" is a Role property on the deployed function, resolved through an ARN, and the policy behind that ARN is a set of JSON documents attached to a role name. Three lookups away from anything in your repo. So the assistant does the only thing it can: it assumes the permissions exist. Ninety percent of the time they do, which is exactly what makes the other ten percent expensive. The failure surfaces at runtime, in an environment where you are reading CloudWatch instead of your editor. Infrawise already builds a graph of your infrastructure and your code. The AST scan produces typed edges from functions to the things they touch: a query or scan edge to a DynamoDB table, a reads secret edge to a Secrets Manager secret, a reads parameter edge to an SSM parameter, a publishes to edge to an SQS queue or an SNS topic. Separately, the Lambda extractor pulls roleArn off every function, then fetches what that role permits. It batches the fetch per unique role ARN, so ten functions sharing one role cost one IAM round trip, not ten. For each role it walks both attached managed policies and inline policies, decodes the policy documents, and reduces every Allow statement to the service prefix of its actions. sns:Publish becomes sns . dynamodb:GetItem becomes dynamodb . A bare " " action becomes . The result is allowedServices , a flat list like "logs", "dynamodb" . Now the comparison is trivial. Walk the function's outgoing edges, map each to the service prefix it requires, and subtract allowedServices . What is left is what the code needs and the role does not grant. The mapping is deliberately narrow. Five edge types map to five services: | Edge | Required service | |---|---| query or scan on a DynamoDB table | dynamodb | reads secret | secretsmanager | reads parameter | ssm | publishes to a queue | sqs | publishes to a topic | sns | Two cases produce no finding at all, on purpose: No IAM data. If the IAM read fails missing permission, SCP, whatever , allowedServices comes back undefined and the analyzer skips the function entirely. Infrawise will not tell you a permission is missing when it could not read the policy. Silence beats a false alarm you learn to ignore. Wildcard. If the role has "Action": " " anywhere, the service list contains and the function is skipped. AdministratorAccess is its own problem, but it is not a missing-permission problem. Two places, and they cover the two moments where it matters. While you are writing the handler. analyze function returns missingPermissions alongside everything else it knows about the function: { "function": "order-processor", "found": true, "file": "src/handlers/order-processor.ts", "triggers": { "type": "sqs", "source": "orders", "eventShape": "event.Records 0 .body" } , "accesses": { "targetId": "table:dynamo:Orders", "edgeType": "query", "targetName": "Orders", "targetType": "table" }, { "targetId": "secret:aws:stripe/live", "edgeType": "reads secret", "targetName": "stripe/live", "targetType": "secret" }, { "targetId": "topic:aws:order-events", "edgeType": "publishes to", "targetName": "order-events", "targetType": "topic" } , "missingPermissions": "secretsmanager", "sns" , "issues": ... , "recommendations": ... } That is the whole point. The assistant asked one question before writing code and got back the trigger event shape and the fact that two of the three services this function talks to are not in its role. It can tell you to fix the policy in the same breath as it writes the handler. Note the field is omitted entirely when IAM data was unavailable, rather than returned as an empty array. An empty missingPermissions means "checked, nothing missing." An absent one means "not checked." Those are different answers and conflating them is how you get misplaced confidence. Before you deploy. The same comparison runs as a high-severity analyzer in infrawise analyze and infrawise check : 1. HIGH Lambda "order-processor" accesses sns but execution role has no sns permissions "order-processor" calls sns in code but its IAM execution role arn:aws:iam::123456789012:role/order-processor-role has no sns: permissions. This will cause AccessDeniedException at runtime — code passes tests but fails in AWS. → Add sns permissions to the execution role for "order-processor". Minimum required: sns:Publish. The recommendation names actual actions, not sns: . Each service has a minimal action set baked in: secretsmanager:GetSecretValue for secrets, ssm:GetParameter, ssm:GetParameters, ssm:GetParametersByPath for parameters, sqs:SendMessage, sqs:ReceiveMessage, sqs:DeleteMessage, sqs:GetQueueAttributes for queues, and the CRUD-plus-query set for DynamoDB. You can paste the list into the policy without thinking about whether you just granted dynamodb:DeleteTable . Because it is a high-severity finding, infrawise check fails the build on it by default. That is the whole CI story: check runs a fresh analysis never a cached graph, since CI gating on stale data is worse than no gating and exits non-zero when anything at or above --fail-on shows up. Default threshold is high . infrawise check Names have to line up. The check joins a code function node to a Lambda node by exact name match. If your handler is exported as handler in src/orders.ts but deployed as order-processor , infrawise finds no code node for that Lambda and produces no finding. It fails closed, which is the right direction, but it means the check is only as good as your naming discipline. If you use a framework that derives function names from the deployed name, you get this for free. Granularity stops at the service prefix. Infrawise compares sns against sns , not arn:aws:sns:us-east-1:123456789012:order-events against the resource in your PublishCommand . A role scoped to one topic passes the check even if your code publishes to a different topic. So this catches "the policy has no SNS at all," which is the common case, not "the policy has the wrong SNS resource." Resource-level checking is a different problem and pretending otherwise would make the finding untrustworthy. It needs read access to IAM. Five actions: iam:ListAttachedRolePolicies , iam:ListRolePolicies , iam:GetRolePolicy , iam:GetPolicy , iam:GetPolicyVersion . They are optional in the infrawise IAM policy. Drop them and everything else still works, you just lose this check. Infrawise is read-only across the board, so nothing here writes to your account. cd your-project npx infrawise start --claude That probes your environment, writes infrawise.yaml and .mcp.json , and opens Claude Code with all 21 tools connected. Ask it to write a Lambda that reads a secret, and watch it check the role before it writes the code. AccessDeniedException in a Lambda is almost never a code bug. It is a drift bug: the code grew a dependency, the execution role did not follow. analyze function before writing a handler to get missingPermissions alongside the trigger event shape. An absent field means IAM was not readable; an empty array means it was checked and clean. infrawise check in CI. Missing permissions are high severity, so the default --fail-on high blocks the deploy.