feat(hooks): context-aware k8s guard (port from codex) + git-safety kubectl handoff - #33
Merged
Merged
Conversation
…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>
There was a problem hiding this comment.
💡 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".
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
Running the T1/T2 e2e gates against a local minikube was impossible: the active k8s context
guard (sagart plugin, allowlist
^kind-) blocks every writekubectlwhose--contextdoesn'tmatch 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.pyso 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/minikubeby cluster context, not by name:--context, or dynamic--contextget/describe/logs/…)prod:list (exact)dev:list (exact match)Invariant
deny > ask > allow. Classification is exact-match (so..._clerumprod and..._clerum-devdev never cross-capture). Faithful copy of codex'scloud_operation_gate.py,minikube_context.py,script_operation_inspector.py; newcontext_classification.pyreads thedev:/prod:lists from the repo'sAGENTS.mdand a low-confidence memory store(
~/.ralph/k8s-context-memory.json) that only elevates unknown→ask, never auto-allow, never degradesprod. Entrypoint mirrors
git-safety-guard.pyI/O and emitspermissionDecision(allow/ask/deny).git-safety kubectl handoff (single owner, no double-decision)
git-safety-guard.pyno longer decides kubectl: kubectl removed fromDESTRUCTIVE_INNER, theSAFE/BLOCKED/CONFIRMATIONaggregates, and the confirmation tier.aws/gcloud/gsutil/rm/gituntouched. Fixes the T1/T2 blocker: a mass delete on a local minikube is now
ask, not a harddenythat 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_commandcollapses quoted-string concatenation ('rm' + ' -rf /').shutil.rmtree/os.removedirson non-temp absolute paths (same temp-direxemption as
rm). Residual limit documented: variable/expression args can't be resolvedstatically — inherent to pattern matching.
Validation
TestKubectlHandedOffdocuments the new contract; aws/gcloud/git/rm intact.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.permissionDecisionenum validated.NOT in this PR (follow-up)
k8s-context-guard-v2in~/.claude/settings.json, sync to~/.claude/hooks/, neutralize sagart viaenv.K8S_GUARD_ALLOWED_CONTEXTS=".", remove the inertk8s-context-guard.shtwin). The guard does not run yet until deployed — the registered guardpoints at the
maincheckout.dev:/prod:block in evenfire/clerumAGENTS.md) + e2e-real-minikube.🤖 Generated with Claude Code