Skip to content

feat(hooks): context-aware k8s guard (port from codex) + git-safety kubectl handoff - #33

Merged
alfredolopez80 merged 6 commits into
mainfrom
claude/k8s-context-guard-allowlist-f0df82
Aug 1, 2026
Merged

feat(hooks): context-aware k8s guard (port from codex) + git-safety kubectl handoff#33
alfredolopez80 merged 6 commits into
mainfrom
claude/k8s-context-guard-allowlist-f0df82

Conversation

@alfredolopez80

Copy link
Copy Markdown
Owner

Context

Running the T1/T2 e2e gates against a local minikube was impossible: the active k8s context
guard (sagart plugin, allowlist ^kind-) blocks every write kubectl whose --context doesn't
match the pattern, and local profiles are named clerum-*. Widening the name allowlist is fragile
(changes per branch) and insecure (trusts the name, doesn't verify the cluster is really local).

This ports the codex-ralph-vault-loop context guard to Claude Code as a global policy, and hands
kubectl off to it from git-safety-guard.py so there is a single owner of the kube domain.

What this adds

New guard — k8s-context-guard-v2 (.claude/hooks/k8s_context_guard/ + entrypoint)

Decides kubectl/helm/minikube by cluster context, not by name:

# Condition Decision
1 kubectl write without --context, or dynamic --context deny
2 pure read (get/describe/logs/…) allow
3 factually-verified minikube (API server == a Running profile) allow (complete delete → ask)
4 obvious-prod pattern or prod: list (exact) deny
5 dev: list (exact match) ask
6 unknown with a memory clarification (low-confidence) ask
7 anything else (undeclared remote) deny (fail-closed)

Invariant deny > ask > allow. Classification is exact-match (so ..._clerum prod and
..._clerum-dev dev never cross-capture). Faithful copy of codex's cloud_operation_gate.py,
minikube_context.py, script_operation_inspector.py; new context_classification.py reads the
dev:/prod: lists from the repo's AGENTS.md and a low-confidence memory store
(~/.ralph/k8s-context-memory.json) that only elevates unknown→ask, never auto-allow, never degrades
prod. Entrypoint mirrors git-safety-guard.py I/O and emits permissionDecision (allow/ask/deny).

git-safety kubectl handoff (single owner, no double-decision)

git-safety-guard.py no longer decides kubectl: kubectl removed from DESTRUCTIVE_INNER, the
SAFE/BLOCKED/CONFIRMATION aggregates, and the confirmation tier. aws/gcloud/gsutil/rm/git
untouched. Fixes the T1/T2 blocker: a mass delete on a local minikube is now ask, not a hard
deny that git-safety could not revert.

Hardening against rm obfuscation (approved addendum)

Validation of rm -rf / surfaced two pre-existing bypasses in git-safety; closed here:

  • normalize_command collapses quoted-string concatenation ('rm' + ' -rf /').
  • New pattern blocks shutil.rmtree/os.removedirs on non-temp absolute paths (same temp-dir
    exemption as rm). Residual limit documented: variable/expression args can't be resolved
    statically — inherent to pattern matching.

Validation

  • 250 passed across git-safety + cloud + restore + the new guard.
  • New guard: 23 unit tests (clerum vs clerum-dev exact, minikube→allow, delete --all→ask, no-context→deny, memory→ask-without-degrading-prod, output shape).
  • git-safety suite updated: kubectl cases retired, TestKubectlHandedOff documents the new contract; aws/gcloud/git/rm intact.
  • Empirically verified on the real edited file: rm -rf / denied in 21/21 scenarios (incl. python -c, perl -e, $(), sh -c); the 2 obfuscation gaps now denied; zero false positives; kubectl ceded.
  • Pre-commit hook: hook JSON formats + permissionDecision enum validated.

NOT in this PR (follow-up)

  • Global deployment (register k8s-context-guard-v2 in ~/.claude/settings.json, sync to
    ~/.claude/hooks/, neutralize sagart via env.K8S_GUARD_ALLOWED_CONTEXTS=".", remove the inert
    k8s-context-guard.sh twin). The guard does not run yet until deployed — the registered guard
    points at the main checkout.
  • Per-repo data (dev:/prod: block in evenfire/clerum AGENTS.md) + e2e-real-minikube.

🤖 Generated with Claude Code

…ubectl handoff

Add k8s-context-guard-v2: a PreToolUse/Bash guard that decides kubectl/helm/minikube
by cluster CONTEXT instead of by name allowlist. Ported from codex-ralph-vault-loop:
factual minikube verification (minikube profile list + API-server match), exact-match
dev/prod classification from the repo's AGENTS.md, static script inspection, and a
low-confidence memory layer that only elevates unknown to ask (never auto-allow, never
degrades prod).

Decision model (deny > ask > allow): no --context flag -> deny; read -> allow; verified
minikube -> allow (complete deletion -> ask); prod (obvious/declared) -> deny; declared
dev -> ask; unknown -> deny (fail-closed). Exact-match classification so clerum vs
clerum-dev never cross-capture.

Hand off kubectl from git-safety-guard.py to this guard (single owner, no double-decision):
remove kubectl from DESTRUCTIVE_INNER and the SAFE/BLOCKED/CONFIRMATION aggregates and the
confirmation tier. aws/gcloud/gsutil/rm/git untouched. This fixes the T1/T2 blocker: a mass
delete on a local minikube is now ask, not a hard deny.

Also harden git-safety against rm obfuscation (approved addendum): collapse quoted-string
concatenation in normalize_command, and block recursive library deletion (rmtree/removedirs)
on non-temp absolute paths.

Tests: 23 unit tests for the new guard; git-safety suite updated (kubectl cases retired,
TestKubectlHandedOff documents the handoff contract). Full suite: 250 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8e5b1abb8c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .claude/hooks/k8s-context-guard-v2.py Outdated
Comment thread .claude/hooks/git-safety-guard.py
Comment thread .claude/hooks/k8s_context_guard/cloud_operation_gate.py
Comment thread .claude/hooks/k8s_context_guard/cloud_operation_gate.py
alfredolopez80 and others added 5 commits August 1, 2026 22:36
…tection, helm/pv, hardening

Addresses the multi-agent review of PR #33 (security / architecture / quality / python / super-auditor),
all reproduced against the real modules and a live minikube:

- BLOCKER (bypass): a leading sudo/timeout/nohup/nice/eval, an opaque shell (dash/ksh/fish),
  or command substitution $()/backtick let a destructive prod kubectl through as allow. Peel
  command-prefix wrappers, route opaque shells + eval to approval, and detect cloud tools
  inside substitution -> approval (never silent allow).
- BLOCKER (minikube detection dead on real minikube): gated on Status=="Running" (minikube
  emits "OK") and used `minikube -p N kubectl -- config current-context`, which reads the
  GLOBAL context. Rewrote verify_minikube_context to match the context against the
  authoritative `minikube profile list` (Status OK/Running) AND require a local server
  endpoint (docker forwards to 127.0.0.1, so an exact server compare is unreliable). Verified
  live: clerum-issue-223 now detected as minikube.
- HIGH (helm/minikube under-protected): writes were only ever ask, even vs prod. Parse helm
  --kube-context and route through the same verify+classify path (prod -> deny); minikube is
  local -> ask.
- MEDIUM: pv/pvc/persistentvolume(s)/persistentvolumeclaim(s) + `no` alias added to the
  complete-deletion set (ask even on verified minikube).
- MEDIUM: AGENTS.md parser dropped the rest of a section after an item ending in ':'.
- MEDIUM: cwd=Path.cwd() import-time default -> resolved per call.
- MEDIUM: rmtree/removedirs hardening extended to from-import, keyword path=, getattr.
- Docstrings aligned (kustomize/read/unparsable); TOOL_RE narrowed to helm|kubectl|minikube;
  stale KUBECTL_* handoff comments corrected; AGENTS.md trust boundary documented.
- Tests: +29 regression tests (wrapper/shell/substitution corpus, helm/minikube routing,
  pv/pvc, parser typo, corrupt memory, unparseable, _choose precedence, rmtree obfuscations).
  Full suite: 285 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…w blocker #1)

Remove the inert k8s-context-guard.sh twin (identical HOOK_NAME, unregistered) and its test
runner — the diagnostic trap flagged by the review. Add scripts/install-k8s-context-guard-v2.sh
which registers v2 as a PreToolUse/Bash hook and neutralizes the legacy sagart context-guard
atomically, so the git-safety handoff never lands without its replacement running (Task #4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n flags)

Pass-2 simplify review reproduced a bypass introduced by the pass-1 wrapper fix: a shared
option table assumed a flag means the same across all wrappers, so `time -p`, `command -p`,
`doas -n`, `setsid -c` and `watch <tool>` swallowed the tool token and reached allow.

Replace the shared table with a per-wrapper boolean-flag map; any option not provably boolean
makes the peel un-inspectable -> approval (fail-closed, never a guessed skip that could hide
the real tool). Derive the substitution scan alternation from CLOUD_TOOLS so it cannot go
stale. +9 regression tests (per-wrapper edge cases + nested wrappers). Suite: 294 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…int spoof

Self re-verification (analysis-only, no live kubectl — current-context is prod) found two
residuals the first two passes missed:
- `cat <(kubectl … delete ns prod)` and `>(…)` process substitution escaped detection (only
  $()/backtick were scanned). Extend the substitution scan to <( and >(.
- _is_local_endpoint('https://10.0.0.1@evil.com') returned True — the naive regex treated the
  userinfo as the host. Use urlsplit().hostname so userinfo/port/IPv6 parse correctly (also
  fixes [::1] recognition).

+ regression tests (process substitution, endpoint-spoof corpus). Suite: 303 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-deny

Codex flagged two real bugs the 4 review passes missed:
- P1: the entrypoint returned exit 1 on deny; a non-zero exit can be treated as a
  non-blocking hook error, so permissionDecision=deny might not block. Return 0 for
  allow/ask/deny (the JSON decision is authoritative).
- P1: in a chained command, _choose picked the most verb-destructive segment, so a prod
  mutation could lose to another segment's dev/ask (kubectl --context=prod apply && kubectl
  --context=dev delete pod). Encapsulate the prod/unknown verdict as block in the assessment
  (the gate now consults classify), so _choose picks the most restrictive across the whole
  chain. Updated 3 tests to the new gate behavior + 2 regression tests.

Codex comments #2 (registration) and #3 (wrapper unwrap) were already resolved by earlier
commits (ba86ce3 deploy scaffolding, 5d7d6a0/df8c9ba wrapper peeling). Suite: 305 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alfredolopez80
alfredolopez80 merged commit 00d6bad into main Aug 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant