GitHub - k-krew/columbo: A minimal CLI to detect potentially forgotten Kubernetes pods using an explainable suspicion score.

4 min read Original article ↗

Claude Assisted CI Go License

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.

Example

$ 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)
----------------------------------------

Features

  • Suspicion scoring — each pod is scored against 9 independent signals; only pods meeting the threshold are shown
  • Explainable results--explain shows exactly which signals fired and why
  • Pre-scoring filters — DaemonSet and StatefulSet pods are always skipped; pods younger than --age-threshold and 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), or 2 (error); use --quiet for exit-code-only mode
  • Multiple output formatstext (tabular) and json

Installation

Homebrew (macOS / Linux)

brew tap k-krew/tap
brew install columbo

From source

git clone https://github.com/k-krew/columbo.git && cd columbo
go build -o columbo .

Binary download

Grab the latest binary from GitHub Releases and place it in your $PATH.

Quick Start

# 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

Flags

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)

Scoring

Columbo scores every pod that passes the pre-filtering stage. Signals are independent and additive.

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

Debug-like substrings (case-insensitive): debug, test, shell, ubuntu, busybox, curl.

Owned pods (Deployment, Job, …) are still scored on all other signals — the +50 only fires for truly bare pods with no ownerReferences.

DaemonSet and StatefulSet pods are skipped before scoring entirely.

Pre-scoring Filters

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

Output Formats

text — tabular (default)

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)
  - ...
----------------------------------------

json — machine-readable

[
  {
    "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)"
    ]
  }
]

Exit Codes

Code Meaning
0 No suspicious pods found
1 One or more suspicious pods detected
2 Execution error (API unreachable, bad flags, …)

Safety

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 get on namespaces is not permitted, columbo assumes the namespace exists and continues — it will not fail on limited RBAC.

Contributing

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.

License

Apache License 2.0 — see LICENSE.