openshift/triage-leaked-infra
Assess whether an AWS VPC or infra set from HyperShift CI is safe to delete. Use when the user pastes cleanleaked output and asks 'can I delete this?', 'is this safe to remove?', 'triage this infra', asks about a LEAKED or UNCERTAIN verdict, provides a VPC ID or infraID and wants to know if it's orphaned, or says 'check this VPC'. Also use when the user asks 'should I delete this?' about any AWS resource in the HyperShift CI account.
npx skills add https://github.com/openshift/hypershift --skill triage-leaked-infra
Assess whether an AWS infrastructure set (VPC + associated resources) from HyperShift CI is safe to delete. Every claim must be backed by an empirical AWS query — never assume, always verify.
The user provides one of:
vpc-0abc123...)00ab3695c5f73d4354b9 or node-pool-78vcg)Extract the infraID and VPC ID from the input. If only one is given, derive the other:
aws ec2 describe-vpcs --vpc-ids <VPC> --query 'Vpcs[0].Tags' → extract infraID from kubernetes.io/cluster/<infraID> tag or Name tag (strip -vpc suffix)aws ec2 describe-vpcs --filters "Name=tag:Name,Values=<infraID>-vpc" → get VPC IDRun these in order. For each, report PASS (safe signal), FAIL (do NOT delete), or UNKNOWN (could not determine). Use --region us-east-1 for all commands.
aws ec2 describe-vpcs --vpc-ids <VPC> --query 'Vpcs[0].Tags' --output json
hypershift.openshift.io/do-not-delete=true → if present: FAILhypershift.openshift.io/ci-cluster → if present: FAIL (this is a management cluster)From the Name tag: if it is hypershift-ci-2-vpc, hypershift-ci-3-vpc, or hypershift-ci-metrics-vpc → FAIL
Check if the infraID contains any of these usernames: aabdelre, agarcial, ahmed, alamela, alesross, bclement, brcox, celebdor, cewong, dario, dari2o, dmace, glipceanu, jiezhao, jparrill, mbhalodi, mbrown, meha, mgencur, mulham, mraee, rkshirsa, sdminonne, sjenning, tsegura, vismishr
If match → FAIL (developer cluster, contact the owner)
From the VPC tags, check expirationDate:
expirationDate tag only exists on resources created in the last ~2 weeks)When expirationDate is absent, determine age from the earliest timestamped sub-resource:
aws ec2 describe-vpc-endpoints --region us-east-1 --filters "Name=vpc-id,Values=<VPC>" --query 'VpcEndpoints[*].CreationTimestamp' --output text
aws ec2 describe-network-interfaces --region us-east-1 --filters "Name=vpc-id,Values=<VPC>" --query 'NetworkInterfaces[*].Attachment.AttachTime' --output text
Take the earliest timestamp found. If the resource is older than 24 hours AND the CI pattern check (step 5) is PASS → PASS. If younger than 24 hours → FAIL. If no timestamp found at all → UNKNOWN.
Does the infraID match a known CI test pattern?
00ab3695c5f73d4354b9) → CI generic e2e → PASScreate-cluster-*, node-pool-*, control-plane-upgrade-*, autoscaling-*, karpenter-*, karpenter-upgrade-control-plane-*, scale-from-zero-*, kms-verify-*, request-serving-*, private-*, proxy-*, spot-demo-*, ha-break-glass-creds-*, custom-config-*, ho-upgrade-*, multi-hop-upgrade-* → PASShc1-*, clust-*, dev-*, test-dev-*) → UNKNOWN (might be a developer cluster)aws s3api head-object --bucket hypershift-ci-oidc --key "<infraID>/.well-known/openid-configuration" --region us-east-1
aws s3api head-object --bucket hypershift-ci-2-oidc --key "<infraID>/.well-known/openid-configuration" --region us-east-1
aws s3api head-object --bucket hypershift-ci-3-oidc --key "<infraID>/.well-known/openid-configuration" --region us-east-1
If any returns 200 → FAIL (cluster still active). If all return 404/error → PASS.
aws ec2 describe-instances --region us-east-1 \
--filters "Name=tag:kubernetes.io/cluster/<infraID>,Values=owned" \
"Name=instance-state-name,Values=pending,running,stopping,stopped" \
--query 'Reservations[*].Instances[*].[InstanceId,State.Name,Tags[?Key==`Name`].Value|[0]]' --output text
If any instances returned → FAIL. If none → PASS.
aws ec2 describe-instances --region us-east-1 \
--filters "Name=vpc-id,Values=<VPC>" "Name=tag:red-hat-managed,Values=true" \
--query 'Reservations[*].Instances[*].InstanceId' --output text
If any instances returned → FAIL (ROSA managed infrastructure). If none → PASS.
Query each and report ID + Name for every resource, not just counts:
aws elbv2 describe-load-balancers --region us-east-1 --query "LoadBalancers[?VpcId=='<VPC>'].[LoadBalancerName,Type,Scheme,DNSName]" --output text
aws ec2 describe-vpc-endpoints --region us-east-1 --filters "Name=vpc-id,Values=<VPC>" --query 'VpcEndpoints[*].[VpcEndpointId,VpcEndpointType,ServiceName,State]' --output text
aws ec2 describe-vpc-endpoint-service-configurations --region us-east-1 --output json | # filter by kubernetes.io/cluster/<infraID> tag
aws ec2 describe-nat-gateways --region us-east-1 --filter "Name=vpc-id,Values=<VPC>" --query 'NatGateways[*].[NatGatewayId,State,Tags[?Key==`Name`].Value|[0],NatGatewayAddresses[0].PublicIp]' --output text
aws ec2 describe-internet-gateways --region us-east-1 --filters "Name=attachment.vpc-id,Values=<VPC>" --query 'InternetGateways[*].[InternetGatewayId,Tags[?Key==`Name`].Value|[0]]' --output text
aws ec2 describe-subnets --region us-east-1 --filters "Name=vpc-id,Values=<VPC>" --query 'Subnets[*].[SubnetId,CidrBlock,AvailabilityZone,Tags[?Key==`Name`].Value|[0]]' --output text
aws ec2 describe-security-groups --region us-east-1 --filters "Name=vpc-id,Values=<VPC>" --query 'SecurityGroups[*].[GroupId,GroupName]' --output text
aws ec2 describe-network-interfaces --region us-east-1 --filters "Name=vpc-id,Values=<VPC>" --query 'NetworkInterfaces[*].[NetworkInterfaceId,InterfaceType,Description]' --output text
aws ec2 describe-route-tables --region us-east-1 --filters "Name=vpc-id,Values=<VPC>" --query 'RouteTables[*].[RouteTableId,Associations[0].Main,Tags[?Key==`Name`].Value|[0]]' --output text
aws ec2 describe-addresses --region us-east-1 --filters "Name=domain,Values=vpc" --output text | # cross-check with NAT gateway EIPs
This check is informational — always PASS but list every resource with its ID and name.
aws route53 list-hosted-zones --output text --query 'HostedZones[*].[Id,Name,Config.PrivateZone,ResourceRecordSetCount]'
Search for zones matching <infraID>.ci.hypershift.devcluster.openshift.com and <infraID>.hypershift.local. For each found zone, list its records:
aws route53 list-resource-record-sets --hosted-zone-id <ZONE_ID> --query 'ResourceRecordSets[*].[Name,Type]' --output text
Report zone IDs, names, private/public, and record count. Informational — always PASS.
aws iam list-open-id-connect-providers --output json
Search for providers whose ARN contains the infraID (pattern: oidc-provider/hypershift-ci-*-oidc.s3.*.amazonaws.com/<infraID>). Report if found — this is an orphaned IAM resource that should be cleaned up with the infra set.
aws iam list-roles --query "Roles[?starts_with(RoleName, '<infraID>')].[RoleName,CreateDate]" --output text
Report any IAM roles whose name starts with the infraID. These are orphaned OIDC-type roles (cloud-controller, ebs-csi, ingress, etc.) that belong to this infra set.
Present the report in this format:
## Triage: <infraID>
| # | Check | Result | Detail |
|---|-------|--------|--------|
| 1 | Protection tags | PASS | No do-not-delete or ci-cluster tag |
| 2 | Protected VPC name | PASS | Name is "abcdef1234-vpc" |
| 3 | Protected user | PASS | No username match |
| 4 | Expiration date | PASS | Expired 2026-07-03 (5 days ago) |
| 5 | CI pattern match | PASS | Hex infraID (e2e-generic) |
| 6 | OIDC S3 liveness | PASS | Not found in any bucket |
| 7 | EC2 instances | PASS | 0 instances |
| 8 | red-hat-managed | PASS | No managed instances |
| 9 | Sub-resources | INFO | 1 IGW, 1 subnet, 1 RTB |
| 10 | Route53 zones | INFO | 2 zones found |
| 11 | OIDC IAM provider | INFO | 1 orphaned provider found |
| 12 | IAM roles | INFO | 0 orphaned roles |
### Verdict: SAFE TO DELETE
This infra set has no protection tags, no running instances, no OIDC document,
and the infraID matches a CI hex pattern. All deletion checks pass.
These are non-negotiable:
Take openshift/triage-leaked-infra from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.