An S3 listing succeeds in your terminal, but a Node.js program listing the same bucket gets AccessDenied. Running on the same computer does not mean both requests use the same AWS identity. First compare who made each call; then compare what action and resource each call requested.
Why might CLI and SDK select different credentials?
The diagram assumes Role A alone has s3:ListBucket for the example bucket and there are no other restrictions. Its 200 and 403 are illustrative results, not responses from a live AWS account.
The CLI accepts --profile per command. By contrast, new S3Client({ region: "ap-northeast-2" }) without explicit credentials follows the Node.js SDK's default credential provider chain. Environment variables, shared configuration, and runtime roles can provide different identities. A CLI command with --profile dev and an SDK process with no selected profile may therefore run as different accounts or roles. AWS documents the SDK credential chain and precedence.
Assume both tools call ListObjectsV2 on an imaginary example-docs-bucket. CLI profile dev selects Role A; the Node process selects Role B. If only Role A can list that bucket, CLI success and SDK denial are consistent.
aws s3api list-objects-v2 --bucket example-docs-bucket --max-keys 1 --profile devimport { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3";
const s3 = new S3Client({ region: "ap-northeast-2" });
const page = await s3.send(new ListObjectsV2Command({
Bucket: "example-docs-bucket",
MaxKeys: 1,
}));
console.log(page.KeyCount);The JavaScript example needs @aws-sdk/client-s3 in the project. Replace the bucket and region for a real comparison, but keep them identical across the two requests while investigating.
Check who actually calls AWS from each process
Use the same profile as the successful CLI command:
aws sts get-caller-identity --profile dev
aws configure list --profile devThe first command reports Account and Arn; the second shows where CLI settings came from. aws configure list may mask part of a key, but do not paste its raw output into a public issue or chat. Share even account IDs and ARNs only when needed.
Now run STS from the Node process that fails. This independent example assumes @aws-sdk/client-sts is installed:
import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
const client = new STSClient({ region: "ap-northeast-2" });
const { Account, Arn } = await client.send(new GetCallerIdentityCommand({}));
console.log({ Account, Arn });Compare account and role in the results. Temporary sessions of the same role can have different session-name suffixes, so compare the role identity rather than requiring the full session ARN strings to match. If account or role differs, inspect the CLI's --profile, Node's AWS_PROFILE, injected environment credentials, and any explicit credentials in code. A dev entry in ~/.aws/config or ~/.aws/credentials does not mean every process selects it automatically. AWS CLI configuration guidance explains profiles and setting sources.
On macOS or Linux, AWS_PROFILE=dev node list.mjs selects a profile for that process. Then run STS again: access-key environment variables or explicit credentials may still take precedence. Confirm the actual identity rather than assuming the profile name solved the issue.
For local development, follow the organization's login method; for a new setup, prefer IAM Identity Center and short-lived credentials. Avoid copying long-lived access keys into code or adding AmazonS3FullAccess merely to make an error disappear.
What if identity matches but AccessDenied remains?
Next compare the requested API action. A successful ListObjectsV2 requires s3:ListBucket on the bucket; an SDK PutObjectCommand needs s3:PutObject on the target objects. AWS maps S3 API operations to policy actions. CLI success on one operation says nothing about a different SDK operation.
Match the account and role, bucket, region, and API action. If those all match and only one request fails, inspect the actual error action and resource, IAM permissions, bucket policy, and applicable organization restrictions. Broadening permissions first can conceal the mismatch.
| Comparison | Inspect next |
|---|---|
| Different account or role | Profile, environment variables, explicit credentials |
| Same identity, different API action | Required policy action and resource for each call |
| Same identity and action | Bucket and region, explicit denies, applicable policies |
Key takeaways
When CLI works but the SDK returns AccessDenied, inspect who called before what was called. Use STS to compare identities, then compare credential sources and the exact S3 action, bucket, and region. Find the differing condition instead of opening broad permissions to hide it.

