Symptom
Running cost:analyze --profile stacks against our stacks AWS profile produces:
Service Resources Cost % of Total
Amazon Simple Storage Service 0 buckets $191.91 62.4%
...
Total: $307.63 across 19 servicesS3 is 62.4% of the bill ($191.91 in April 2026), but listBuckets() reports 0 buckets. That's not internally consistent — you can't be charged $191/mo for S3 with zero accessible buckets.
What's happening in code
packages/ts-cloud/bin/commands/cost.ts (registerCostCommands → cost:analyze):
let s3Buckets: number | null = null
if (services.some(s => s.service === S3_SERVICE_NAME)) {
try {
const result = await new S3Client('us-east-1', profile).listBuckets()
s3Buckets = result.Buckets?.length ?? 0
}
catch {
// listBuckets needs s3:ListAllMyBuckets — silently fall back to '-'
}
}So listBuckets() either:
- Succeeded but returned
Buckets: undefinedorBuckets: []→ we report0 buckets. But the account is being billed for S3. - Threw → we'd fall back to
-, not0. Since we report0, the call succeeded.
Hypotheses (in rough order of likelihood)
- The
stacksprofile is scoped to an IAM role/user that can reach Cost Explorer (account-wide billing data) but cannot list buckets. ListBucket might be silently authorized but return an empty list on a permissions edge case rather than throwing. new S3Client('us-east-1', profile)is binding to the wrong region/endpoint and getting an empty bucket list there even though buckets live elsewhere. Note: ListBuckets is supposed to be a global API (region-agnostic), but our client may be region-specific in a way that affects the response.- The S3Client implementation itself has a bug that swallows the actual bucket list (parsing, signature, header).
- Buckets are owned by a sub-account in an Organization and the
stacksprofile credentials are at a level that sees billing roll-up but not the sub-account's buckets.
Repro
cd packages/ts-cloud
bun bin/cli.ts cost:analyze --profile stacks…against any AWS profile where the Cost Explorer report shows non-trivial S3 spend but listBuckets() returns 0.
Acceptance
- Identify which hypothesis is correct.
- Either fix the
S3Clientbug, fix the region binding, or improve thecost:analyzeoutput to distinguish "0 buckets visible" from "can't enumerate buckets — IAM scoping" (don't quietly report0when we can't actually tell). - Bonus: if it's an IAM scoping issue, update the docs / README to call out which IAM perms
cost:analyzeactually needs to be useful (ce:GetCostAndUsage+s3:ListAllMyBucketsat minimum).
Related
- #103 — original AWS bill investigation; this anomaly is the immediate follow-up.
packages/ts-cloud/src/aws/s3.ts— S3Client implementation.