Getting started
From nothing to a ranked list of what you could remove, usually in under an hour. Nobody is emailed and nothing changes until you say so.
1. Connect a cloud, read-only
Go to Sources and add one. Each form shows the exact access it needs and tests it before saving.
- AWS. A role that trusts your workspace, with no key to store. The form gives you the Terraform or CloudFormation, with your workspace's subject already in it. An IAM user with the read-only policy works too. To cover a whole AWS Organization, put the same role in each member account and give its name as "Role name in member accounts".
- Databricks. A service principal with a federation policy, so there's no secret to store. It can view compute and jobs, with SELECT on system.billing, system.query and system.compute. A token or secret works too.
- Snowflake. A service user with workload identity, so there's no key to store, and a role that reads ACCOUNT_USAGE. The form gives you the SQL. A key pair works too. ACCESS_HISTORY needs Enterprise edition, and without it table reads are unknown, so tables aren't called unused.
- Google Cloud. Workload identity, with no key to store. The form gives you the Terraform or gcloud commands, with your workspace's subject already in them. It uses a service account with narrow viewer roles, on the projects or organization, which can't change anything or read your data. A service account key works too.
- Azure. A federated credential on an application with Reader, Monitoring Reader and Cost Management Reader, with no secret to store. The form gives you the Terraform or Azure CLI commands. A client secret works too.
- Kubernetes. Nothing connects in. Apply the manifest the form gives you, and a job in the cluster reports what's there every three hours: names, labels, sizes and usage, never environment variables or secrets. The script is in the manifest for you to read.
- MongoDB Atlas and Confluent Cloud. A read-only service account in each.
- New Relic. A user API key from someone with the Authentication Domain Manager role. It finds billable users who haven't logged in.
- Cloudflare. A token with Account Analytics Read, Workers R2 Storage Read, Zone Read and Zone Analytics Read.
- DigitalOcean. A read-only personal access token.
- Microsoft 365 licenses. An application with the Graph permissions User.Read.All, AuditLog.Read.All and LicenseAssignment.Read.All. Sign-in activity needs Entra ID P1 or P2, and without it only turned-off accounts and unassigned licenses are found. Costs are list prices unless you give yours.
- Salesforce licenses. An external client app with the client credentials flow, running as a read-only integration user who can view setup, and what you pay per user.
- GitLab and Okta seats. Give the source a price per seat. GitLab also needs a token from a group Owner.
- Google Workspace seats. A service account with domain-wide delegation for admin.directory.user.readonly, and what you pay per user.
- Linode. A personal access token with Read Only access to Linodes, Volumes, NodeBalancers and Images. A quiet instance means under 3% CPU over the last day and under a gigabyte of traffic in two months.
- Vultr. The account's API key. Vultr's keys can't be limited to reading, so restrict the addresses it works from. Underrunner only ever reads with it.
- Hetzner Cloud. An API token with Read permission, one per project. Amounts are in the currency Hetzner bills you in.
- Oracle Cloud. A user with an API key, in a group with a read-only policy. The form gives you the policy. Activity comes from Monitoring, so an instance without the Oracle Cloud Agent is never called idle.
- Jira. Optional. A Jira Cloud account that can create issues in one project, and an API token. A removal waiting on a person opens a ticket with the steps, and it closes once a scan confirms the thing has gone.
Costs are measured where the platform says what each resource costs, and marked as an estimate where it doesn't. For exact AWS costs after discounts, point it at a Cost and Usage Report in CSV form.
If you limit connections by address, as a Snowflake network policy, a Databricks IP access list or a mail server might, Underrunner connects from 46.4.105.186 and 2a01:4f8:141:23e::2.
2. Tell it who's who
Ownership decides who gets asked, so it's worth a few minutes.
- Tags are the strongest clue, including OMD tags like
omd:teamandomd:owner. You choose which tag names count, in Settings. - GitHub or GitLab shows where a resource is defined, who last changed that code, and CODEOWNERS. It also brings in teams and their members.
- Confluence finds service catalogs, ownership tables and team pages, and looks up each resource's name in them.
- Okta, Microsoft Entra ID or Google Workspace keeps the org chart current. Departments become teams, managers come through, and anyone who has left is known to have left, so what they owned goes to their manager.
- Backstage, PagerDuty or the ServiceNow CMDB already say who owns many of your services. A resource named after one of those services goes to its team or owner, and it says where that came from. Retired CMDB items are ignored.
- A people CSV with email, name, team, parent team, manager and whether they head the team is the quickest way to give it an org chart.
- Who looks after a source. Each cloud you connect can name a team or a person, for anything from it with no clearer owner. Often one team does look after all of Snowflake, or all of Microsoft 365.
- Owner rules in Settings cover the rest: "anything named
payments-*belongs to Payments".
The People screen also finds what things with no owner have in common, like a name, a tag or an account, and suggests whose they are from what's known about their neighbours. Assigning a group adds an owner rule, so anything new that matches goes to the same place. Below that, it lists the biggest things still with no clear owner, with a best guess you can accept in one click.
3. Define the scope
A scope is a team, an org, or all of engineering. Everything is then inside it, outside it, or unsure. Only "inside" can be asked about or removed. Preview a scope before saving it to see how each resource falls.
4. Read the queue
The Queue ranks everything by savings times how likely it is to be unneeded. Open one to see the measurements, where they came from, and a command to check them yourself. Filters use the same language everywhere:
type:ebs_volume env:dev cost>50team:payments -env:prod idle>30tag:cost-center=1234 age>90name:legacy-* managed:terraform"exact words"matches a name, ID or reason
Is anything pointing at it? It reads the DNS records in Route 53, Cloudflare, Google Cloud DNS, Azure DNS, DigitalOcean, Linode and Vultr. A load balancer, address or machine that a name still points at says so and ranks lower, whichever cloud it's in, and one that nothing points at says that too. A removal stops if a new name has appeared since it was approved. With Jira connected, an open ticket that mentions something and has moved in the last 90 days counts the same way.
5. Decide how careful to be
Three settings do most of the work, and the safe choice is the default for each.
- Emails: review first. You read each email before it goes. Switch to automatic when you trust it, or off to use the Queue alone.
- Removal: dry run. Approved items are checked and rehearsed, and nothing changes. Runbook sends the owner the exact steps. Live does it, using a second credential you add on purpose.
- Production: by hand. Automatic removal in production stays off unless you turn it on.
Connect mail in Settings, with your own mail server or by verifying your domain. To reach people on Slack instead, add a bot token there. Anyone not found in Slack gets the email.
6. What happens to each resource
- Identified. It looks unused, and it's in the queue.
- Clarification. Its owner is asked, in one weekly message, biggest things first. Similar small things are grouped so one answer covers them. No answer brings one reminder, then their manager or team head.
- Final confirmation. The head of that org sees who confirmed it and approves. Until a head is named, this goes to the fallback approver in Settings, and failing that, to whoever set up the workspace.
- Scheduled. Everyone involved gets notice and a way to stop it. It can be switched off first, so anyone relying on it notices.
- Removed, then verified. On the day, everything is checked again against current data. A later scan confirms it's gone, and only then does the saving count.
What a scan costs you
Reading isn't always free, so here's what your own bill sees.
- AWS. CloudWatch charges $0.01 per 1,000 metrics requested, and Underrunner asks for a few per resource. That's roughly 30 cents a scan for 10,000 resources. Cost Explorer charges $0.01 a request, and a scan makes a handful. The first scan also reads three months of daily spend, once, so the charts have history from day one. That's usually a few requests more. CloudTrail lookups are free.
- Snowflake. A few minutes of the small warehouse you point it at, which then suspends itself.
- Databricks. The SQL warehouse you point it at runs while it reads the system tables. A small serverless one is ideal.
- Google Cloud. Reading a BigQuery billing export is capped at 500 GB a scan, whatever the table's size.
- Everything else. The read APIs are free or cost fractions of a cent a call.
Scans run once a day unless you change it under Settings.
Terraform and other infrastructure code
Anything defined in code goes through the code, because deleting it directly only lasts until the next apply. Add a token with write access to your infrastructure repositories on the GitHub or GitLab source, and Underrunner opens a pull request removing the block once it's approved. Merging and applying it removes the resource. Closing it counts as keeping it. Without the token, the owner gets the change by email. You can see the change in advance on any resource.
Your own rules
In Settings, "Your rules" lets you flag what only you know about ("sandbox, and over a month old"), say who owns what, and protect anything from ever being flagged. Tags like do-not-delete also protect a resource, and the list of them is yours to change.
Connecting an LLM
Optional. With an OpenAI or Anthropic key, flagged resources get a plain explanation of what they probably are and what to ask. You set a monthly budget. It can nudge the ranking a little, and can't advance or approve anything.
Signing in
Single sign-on works with any OpenID Connect provider, once you've verified your email domain. Two-step sign-in with an authenticator app is in Settings for everyone else.
Create an account · Try the demo first · How we handle what you connect
API and webhooks
Make a token in Settings and send it as Authorization: Bearer urt_... on any GET under /api, like /api/candidates or /api/resources?q=provider:aws. Tokens can only read.
A webhook gets one POST for every stage change, in order, retried until your server answers 2xx. Each has the headers x-underrunner-timestamp and x-underrunner-signature. To check it, compute HMAC-SHA256 of timestamp.body with your signing secret, compare it with the hex after sha256=, and refuse anything with a timestamp over five minutes old.
{"id": 812, "at": 1793000000000, "event": "approved", "from_stage": "confirmation", "to_stage": "scheduled", "actor": "priya@acme.com",
"candidate_id": 41, "savings_per_month": 3910.0, "detail": "",
"resource": {"id": 77, "name": "ml-feature-store-v1", "type": "rds_instance", "provider": "aws", "account": "4829...", "region": "us-east-1", "native_id": "...", "team": "Machine Learning"}}