Workflow
Scan, plan, apply
Each step writes a file the next one reads. Nothing in your account changes until you have read the plan and confirmed it.
$ tagctl scan ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Tag Compliance Report 2026-02-02 10:51:48 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Overall: 65% compliant ░░░░░░░ (98/150 resources) By Account: ACCOUNT TOTAL COMPLIANT COMPLIANCE aws/production 100 85 85% ░░ aws/staging 50 13 26% ░░░░░░░░ By Required Tag: TAG STATUS PRESENT MISSING INVALID COMPLIANCE environment PASS 140 10 0 93% ░ cost-center FAILED 98 52 5 62% ░░░░ owner FAILED 75 75 10 43% ░░░░░░ Run 'tagctl plan' to see suggested fixes. Detailed results saved to: • JSON: output/scan-20260202-105148.json • CSV: output/scan-20260202-105148.csv • HTML: output/scan-20260202-105148.html
$ tagctl plan Planned changes: aws_instance (web-prod-api-1) + environment: "prod" (inferred: name contains '-prod-') + owner: "[email protected]" (default) aws_s3_bucket (legacy-data-2019) + owner: "[email protected]" (default) aws_db_instance (billing-prod-db) + environment: "prod" (inferred: name contains '-prod-') + owner: "[email protected]" (default) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Summary: 3 resources will be modified 5 tags will be added Run 'tagctl apply' to execute this plan.
$ tagctl apply Applying plan from output/plan-20260202-105301.json Plan created at: 2026-02-02 10:53:01 Changes to apply: • 3 resources will be modified • 5 tags will be added • 0 tags will be updated • 0 tags will be removed Do you want to apply these changes? [y/N]: y Applying changes... [1/5] aws_instance.web-prod-api-1 (environment: prod) ✓ [2/5] aws_instance.web-prod-api-1 (owner: [email protected]) ✓ [3/5] aws_s3_bucket.legacy-data-2019 (owner: [email protected]) ✓ [4/5] aws_db_instance.billing-prod-db (environment: prod) ✓ [5/5] aws_db_instance.billing-prod-db (owner: [email protected]) ✓ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Applied successfully: 5 changes Errors: 0 Duration: 2.184s Run 'tagctl scan' to verify compliance.
-
web-prod-api-1
+ environment: prodinferred from
-prod-
-
etl-stg-worker
+ environment: staginginferred from
-stg-
- legacy-data-2019 + owner: platform-teamdefault when owner is absent
Rules
Fix what the name already tells you
Infer tags from resource names with patterns you define, and fill the rest with defaults when a condition holds. Every proposed change lands in a plan first.
Normalize
One spelling per value
prod, Production and PROD split one cost report into three buckets. tagctl normalize finds the variants and proposes the spelling your policy allows.
- "Production"5 resourcescasing
- "PROD"3 resourcesabbreviation
- "prod"2 resourcesabbreviation
Enforce
Catch missing tags in the pull request
Cost allocation tags are not retroactive. Spend on an untagged resource can never be billed to a team, however well you tag it afterwards. Check the Terraform plan before apply and fail CI when compliance slips.
- Terraform plans and state, honouring
default_tags - SARIF for GitHub code scanning, JUnit for any CI
- OCSF 1.4 findings for Security Lake or any OCSF-native SIEM
Policy as code
One YAML file, reviewed like any other change
Required tags, allowed values, patterns, inference rules and defaults all live in tagctl.yaml. Keep it in git and run the same policy on your laptop and in CI.
tagctl initwrites a templatetagctl validatechecks it before a scan
clouds: aws: - profile: production - profile: staging regions: [us-east-1] policy: required: - name: environment values: [dev, staging, prod] - name: cost-center pattern: "^[A-Z]{2,4}-\\d{3,6}$" rules: infer: - tag: environment from_name: - pattern: "-prod-" value: prod defaults: - resource: "*" when: { tag:owner: absent } set: { owner: [email protected] }
Cost
Put a price on missing tags
tagctl cost joins your policy with Cost Explorer and reports the spend nobody can be billed for, worst tag first.
cost-center20% attributedowner90% attributedAt this rate, 1,794,053 USD a year of spend nobody can be billed for.
tagctl costCoverage
106 AWS resource types, every enabled region
The same AWS services Prowler audits, read in bulk through the Resource Groups Tagging API. Global discovery is the default; narrow it with --region.
- AWSAvailable
- KubernetesComing soon
- GCPComing soon
- AzureComing soon
- EC2
- S3
- CloudTrail
- GuardDuty
- WAF
- Bedrock
- SageMaker
- CodePipeline
- WorkSpaces
- IAM roles
Questions, answered
Something missing? Open an issue on GitHub.
How is this different from Cloud Custodian, AWS Tag Policies or AWS Config?
Custodian is a general policy engine with a lot to learn before it does anything for tags. Tag Policies standardise keys and values in AWS Organizations but do not fix the resources you already have. Config bills per rule evaluation. tagctl does one job: find tag problems, propose fixes, apply them.
Can tagctl change a resource without me seeing it first?
No. apply only executes a plan file and asks for confirmation before it writes. scan and plan never modify anything and run with a read-only IAM policy.
Which AWS credentials does it use?
The standard SDK credential chain: profiles, SSO, environment variables or an instance profile. Pass --role to assume a role, with --external-id and --mfa-serial when you need them.
Which regions does a scan cover?
Every enabled region by default. Limit it with regions in tagctl.yaml or with --region on the command line.
Does it work with Prowler?
The AWS provider covers the same services Prowler audits, and tagctl evaluate checks resources from external JSON against your policy.
What does it cost?
tagctl is free and open source under Apache 2.0. The cost command calls the Cost Explorer API, which AWS bills per request.
Tag your cloud the way your policy says
Install with Go 1.24 or later, or download a prebuilt binary for Linux, macOS or Windows.
go install github.com/unicrons/tagctl/cmd/tagctl@latest