Read only Azure scripts: why these tools never write to Azure
A client gives you access to a subscription on a Tuesday. You have not seen it before. There are four hundred resources, two of which are named in a way that suggests production, and nobody available this week can tell you which.
That is the situation these tools were designed around, and it settles the design question immediately. Read only azure scripts can run on that Tuesday. Anything that writes cannot, because writing requires knowing what you are touching, and knowing what you are touching is the thing you do not yet have.
Read only azure scripts and blast radius, the boring reason
A script that can delete resources has a worst case involving somebody's production data. A script that reads has a worst case involving an API rate limit.
That asymmetry does not go away with careful coding. Every safety mechanism you
add to a destructive script is another thing that can be wrong: a -WhatIf that
somebody omits, a confirmation prompt that gets -Confirm:$false in a scheduled
job, an exclusion list that does not match what the author assumed. The safest
delete logic is the delete logic that is not there.
So the tools produce a list and a cost, and a human deletes things. That is
slower and it is the correct division of labour: the script is good at
enumerating four hundred resources and pricing them, and terrible at knowing
that disk-sqlbackup-temp is genuinely temporary.
The consultant trust problem
The second reason is commercial rather than technical, and it is the one that decided it.
If you are a consultant, an MSP, or simply the new person, the access conversation happens before the value conversation. Asking for Contributor across a subscription on day one is a reasonable request that will slow you down by a week while somebody senior decides how they feel about it.
Asking for Reader is nearly frictionless, because the answer to "what is the worst thing this can do" is "read a list of resources". Being able to say the tooling cannot write, as a property of the tooling rather than a promise about your intentions, is worth more than any amount of reassurance.
The permissions that actually run the AzClean Toolkit are Reader at the scopes you want covered, plus directory read for the credential and RBAC checks. Microsoft's built-in roles reference sets out what Reader permits, which is worth linking when somebody asks, because "Reader" sounds vaguer than it is.
Found nothing is not the same as could not look
This is the design decision I would defend hardest, and it is the least visible.
A cost script authenticates, enumerates, finds no orphaned resources, and prints a clean report. Good news.
A cost script fails to authenticate, enumerates nothing, finds no orphaned resources, and prints a clean report. Identical output. Catastrophically different meaning.
The second case is worse than a crash, because a crash gets investigated and a clean report gets filed. Somebody tells a client the estate is tidy. A scheduled job goes green for eight months while the token it depends on has been expired for seven of them.
So the scripts exit non-zero when they cannot do the job, and the distinction is
enforced in a specific place. The connection check does not test whether a
context object exists; Get-AzContext returns a fully populated context even
when the cached token behind it has expired, which is exactly the trap. It
acquires a token, and treats failure to acquire one as a failure of the run
rather than an absence of findings.
function Assert-AzConnection {
$ctx = Get-AzContext
if (-not $ctx -or -not $ctx.Account) {
Write-Error 'Not connected. Run Connect-AzAccount.'
exit 1
}
# A populated context is not proof of a valid token. Force an acquisition.
try { $null = Get-AzAccessToken -ErrorAction Stop -WarningAction SilentlyContinue }
catch { Write-Error "Cannot acquire a token: $($_.Exception.Message)"; exit 1 }
return $ctx
}
The same principle applies to anything scheduled, and it is the subject of a separate note on exit codes and the silent zero. A script that always exits 0 is a script whose monitoring is decorative.
What read-only does not protect you from
Worth stating plainly, because "read-only" gets treated as a synonym for "safe" and it is narrower than that.
Reading is still access. A Reader on a subscription can enumerate resource names, configurations, tags and network topology. Resource names alone often disclose more than people expect: customer names in resource groups, project codenames, the fact that you run a particular database engine. Granting Reader is a smaller decision than granting Contributor, not a null one.
It can still take a subscription down. Not by writing, but by volume. An unthrottled enumeration across nine subscriptions with a few thousand resources will hit ARM rate limits, and hitting them affects other callers in the same subscription, including deployments. The tools page a request at a time and back off on 429 for that reason, which is unglamorous and the difference between a tool you can run at 10am and one you cannot.
It cannot tell you a finding is safe to action. The output says a disk is unattached. It does not say the data is unwanted, and the read-only guarantee offers no protection whatsoever against somebody deleting the wrong thing off the back of a correct report. The guarantee is about the tool, not about the afternoon.
What this costs
Being honest about the trade-off, because a design note that only lists advantages is marketing.
You do the deleting. For fourteen unattached disks that is fourteen portal operations or a short script you write yourself, and it is genuinely more work than a tool that cleaned up as it went.
There is no remediation automation to buy. If what you want is something that enforces a tagging policy or removes orphans nightly, this is not it, and Azure Policy with deployIfNotExists is a better fit for that job.
And a read-only tool cannot verify its own recommendation. It tells you the disk is unattached. It cannot tell you the deletion succeeded, so the loop is closed by re-running it rather than by a confirmation.
Those are real costs. They buy a tool that can be pointed at any subscription by anybody without a risk assessment, which for the way this work actually happens has been worth it every time.
Where this sits
Everything in the azure security notes and the azure cost notes is checkable this way, which is not a coincidence: the checks were chosen partly because they can be answered by reading. The categories that would need write access, such as remediating a policy or rotating a credential, are deliberately absent.
The licence terms ship inside each download rather than living on a page that can change, which is the same instinct applied to a different problem: the thing you are relying on should travel with the artefact.