columbo is a read-only Kubernetes CLI that finds pods that are likely forgotten or abandoned. Instead of guessing, it calculates a suspicion score - the higher the score, the more likely the pod is unused, manually created, or left behind.
$ columbo
NAMESPACE POD SCORE AGE
default nginx-debug 125 12d
kube-system nsenter-75l64h 75 419d
default network-multitool 70 483d$ columbo --explain
NAMESPACE POD SCORE AGE
default nginx-debug 125 12d
default/nginx-debug score: 125
- no ownerReferences (+50)
- no Service match (+10)
- no labels (+10)
- namespace=default (+5)
- debug-like name ("debug") (+15)
- serviceAccountName=default (+5)
- phase=Failed (+20)
- no CPU/memory requests or limits (+10)
----------------------------------------Identifying abandoned resources in a Kubernetes cluster is hard. Labels rot, owners leave, and standard dashboards show you what is running, but not why. Columbo automates the detective work by looking for classic signs of neglect (no owners, missing services, default namespaces, old age, and failure states). By mathematically scoring these signals, Columbo helps platform teams reclaim wasted resources and reduce clutter without blindly deleting workloads.
- Suspicion scoring — each pod is scored against 10 independent signals; only pods meeting the threshold are shown
- Explainable results —
--explainshows exactly which signals fired and why - Pre-scoring filters — DaemonSet and StatefulSet pods are always skipped; pods younger than
--age-thresholdand pods with an ignore annotation are excluded before scoring - Read-only — columbo never modifies or deletes anything in the cluster
- CI-friendly — exits
0(clean),1(suspicious pods found), or2(error); use--quietfor exit-code-only mode - Multiple output formats —
text(tabular) andjson
go install github.com/k-krew/columbo@latestbrew tap k-krew/tap
brew trust k-krew/tap
brew install --cask columbogit clone https://github.com/k-krew/columbo.git && cd columbo
go build -o columbo .Grab the latest binary from GitHub Releases and place it in your $PATH.
# Scan all namespaces with defaults (threshold 70, age > 7d)
columbo
# Limit to one namespace
columbo -n default
# Lower the threshold to catch more pods
columbo --threshold 30
# See why each pod scored the way it did
columbo --explain
# Machine-readable output
columbo -o json
# JSON with scoring breakdown
columbo -o json --explain
# CI mode — no output, exit code only
columbo -q
echo $? # 0 = clean, 1 = suspicious pods found, 2 = error| Flag | Short | Default | Description |
|---|---|---|---|
--kubeconfig |
~/.kube/config |
Path to kubeconfig file | |
--context |
current context | Kubernetes context to use | |
--namespace |
-n |
all namespaces | Limit to a specific namespace |
--threshold |
70 |
Minimum score to include in output | |
--age-threshold |
7d |
Minimum pod age to consider (1h, 24h, 7d, …) |
|
--ignore-annotation |
orphan-check/ignore |
Exclude pods carrying this annotation with value "true" |
|
--output |
-o |
text |
Output format: text or json |
--explain |
false |
Show per-pod scoring breakdown | |
--quiet |
-q |
false |
Suppress all output (exit code only) |
--config |
Path to a .columbo.yaml configuration file |
Columbo scores every pod that passes the pre-filtering stage. Signals are independent and additive.
The table below shows the default point values. Every value can be overridden via a configuration file.
| Signal | Points |
|---|---|
No ownerReferences (bare / manually created pod) |
+50 |
| No Service selector matches the pod's labels | +10 |
| No labels | +10 |
Namespace is default or test |
+5 |
Phase is Failed or Succeeded |
+20 |
Any container (or init container) in CrashLoopBackOff |
+15 |
| No CPU/memory requests or limits on any container | +10 |
| Pod name contains a debug-like substring | +15 |
serviceAccountName is default (or empty) |
+5 |
Any container image uses :latest or has no tag |
+10 |
Debug-like substrings (case-insensitive): debug, test, shell, ubuntu, busybox, curl.
Owned pods (Deployments, Jobs, …) are still scored on all other signals — the +50 only fires for truly bare pods with no ownerReferences.
Jobs and CronJobs: Pods owned by Jobs are fully scored. Because old, forgotten Jobs often linger in Succeeded or Failed states, they naturally rack up points (e.g., +20 for Terminal Phase, +5 for default namespace). If an abandoned Job accumulates enough points, Columbo will flag it for cleanup.
DaemonSet and StatefulSet pods are skipped before scoring entirely.
Pods are filtered before scoring — they never receive a score and never appear in output:
| Filter | Condition |
|---|---|
| Too young | Pod age < --age-threshold |
| Ignored annotation | Pod has --ignore-annotation key with value "true" (case-insensitive) |
| Managed controller | Pod is owned by a DaemonSet or StatefulSet |
Signal weights and the score threshold can be overridden with a YAML file. Pass it via --config:
columbo --config .columbo.yamlPriority for threshold: --threshold flag > threshold in config file > built-in default (70).
All fields are optional. Omitted fields keep their default values.
# .columbo.yaml
threshold: 80
weights:
no_owner_refs: 60 # default: 50
no_service_match: 10
no_labels: 10
suspicious_namespace: 5
terminal_phase: 20
crash_loop_backoff: 15
no_resources: 10
debug_name: 15
default_service_account: 5
latest_image_tag: 10Setting a weight to 0 effectively disables that signal.
NAMESPACE POD SCORE AGE
default nginx-debug 125 12d
kube-system nsenter-75l64h 75 419d
With --explain:
default/nginx-debug score: 125
- no ownerReferences (+50)
- no Service match (+10)
- debug-like name ("debug") (+15)
- ...
----------------------------------------
columbo -o json[
{
"namespace": "default",
"pod": "nginx-debug",
"score": 125,
"age": "12d"
}
]With --explain the "reasons" array is included:
[
{
"namespace": "default",
"pod": "nginx-debug",
"score": 125,
"age": "12d",
"reasons": [
"no ownerReferences (+50)",
"no Service match (+10)",
"debug-like name (\"debug\") (+15)"
]
}
]| Code | Meaning |
|---|---|
0 |
No suspicious pods found |
1 |
One or more suspicious pods detected |
2 |
Execution error (API unreachable, bad flags, …) |
columbo is strictly read-only. It only calls LIST and GET on pods, services, and namespaces. It never creates, updates, or deletes any resource.
Minimum required RBAC:
rules:
- apiGroups: [""]
resources: ["pods", "services", "namespaces"]
verbs: ["get", "list"]If
getonnamespacesis not permitted, columbo assumes the namespace exists and continues — it will not fail on limited RBAC.
Contributions are welcome! Feel free to open issues and pull requests. Whether it's a bug fix, new feature, documentation improvement, or just a question - all input is appreciated.
Apache License 2.0 — see LICENSE.