fix(security): port permission hardening from master + Copilot CLI adaptation#2286
Merged
Conversation
- Decompose compound commands for permission checks (newline, background &, subshell `( )`) in addition to &&, ||, ;, | so hidden segments are checked.
- `contains_unattestable_construct`: flag command/process substitution and file-target redirects (fd-dup `2>&1` and /dev/null exempt) — RTK can't decompose these, so they are never auto-allowed.
- Route every host hook (Claude, VS Code, Gemini, Cursor, Copilot CLI) through a single decision flow. Precedence: Deny → (defer if unattestable) → Allow → Ask → Default. Auto-allow only on a positive Allow; otherwise defer to the host's own engine.
- Gemini: ask_user instead of hardcoded allow. Cursor: empty `{}` delegation, since permission:"ask" is not enforced on its sandboxed shell.
pszymkowiak
approved these changes
Jun 5, 2026
thehoff
added a commit
to thehoff/contextcrawler
that referenced
this pull request
Jun 7, 2026
…i#2286 port) Port upstream rtk-ai/rtk rtk-ai#2286 (952245d + e16aa26) into the permission gate, reconciled with the fork's existing &/newline split (22890aa). The gate now refuses to auto-allow any command segment it cannot attest: command/process substitution ($(...), backticks, <(...)/>(...)) and real file-WRITE redirects (>file, >>file, >&word, &>file) downgrade Allow -> Ask so the user decides. fd-dups (2>&1, >&2, 2>&-), /dev/null sinks, arithmetic $((...)), variable expansion and input redirects (<, <<, <<<) stay evaluable. Decomposition (split_for_permissions) breaks segments on &&/||/;/| plus background & and newline (22890aa) AND subshell ( ), truncating each segment at its first redirect, so a deny-ruled command hidden in any segment is still checked. Deny is evaluated first across all segments and always wins over the not-evaluable downgrade. The downgrade lives in check_command_with_rules, so BOTH entry paths inherit it: the live hook path (hook_cmd) and the legacy rewrite path (rewrite_cmd via check_command). Dropped rtk-ai#2286's Gemini/Cursor config-scoping parts (N/A here). Produced by an A/B bake-off of two independent implementations, cross-reviewed by the council (codex + mmax). Winner: the verdict-layer-centralised design; grafted the spec-correct input-redirect scope from the runner-up. Final diff council-reviewed (quorum, "no bypasses"). Security suite: subshell/substitution/background/newline/redirect hidden-deny all caught; substitution + file-write redirect never auto-allow; fd-dup and single-quoted literals stay allowable. 3019 tests pass (7 pre-existing hooks::integrity /tmp-mode env failures unrelated). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thehoff
added a commit
to thehoff/contextcrawler
that referenced
this pull request
Jun 7, 2026
…e commands [SECURITY] The rtk-ai#2286 never-auto-allow Ask verdict was silently dropped on the live hook path for commands with no contextcrawler rewrite — letting the host auto-allow unattestable commands via an existing allow rule (e.g. Bash(git:*)). Found by empirical testing of the built binary: `git status $(whoami)` and `git log > /tmp/evil` produced EMPTY hook output (PayloadAction::Skip), so Claude Code fell back to its native Bash(git:*) allow and ran the substitution/redirect unprompted. check_command_with_rules correctly returned Ask, but process_claude_payload_with_gate's get_rewritten()==None branch only escalated to an explicit Claude `ask` when a tirith/supply-chain gate fired (gate_ask) — it ignored a bare permission verdict of Ask. The unit tests passed because they checked the verdict function directly and never exercised the hook's None-branch handling of that verdict. Fix: - hook_cmd.rs (live): the no-rewrite branch now asks when `gate_ask || verdict == PermissionVerdict::Ask`, with a rtk-ai#2286-specific reason ("not auto-evaluable … deferring to you"). Benign Default/no-match commands still Skip silently (no over-escalation); Deny still wins (handled earlier). - rewrite_cmd.rs (legacy): the None arm now exits 3 (ask) instead of 1 (passthrough) for an Ask verdict; Default still exits 1. Exit-code doc updated. - audit_tag generalised ask:gate_no_rewrite -> ask:no_rewrite (now covers both the gate and the verdict cases; no downstream consumer parses it). Empirically verified post-fix: git status$(whoami) / `id` / >file / >&file all return permissionDecision "ask"; plain git status still "allow"; benign non-rewritable still skips. council-reviewed (codex + mmax): leak closed, no over-escalation, Deny wins, no regression. 3030 tests pass (7 pre-existing hooks::integrity /tmp-mode env failures unrelated). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thehoff
added a commit
to thehoff/contextcrawler
that referenced
this pull request
Jun 7, 2026
… pivot Tag the current develop as the fork's first own release (contextcrawler-v0.2.0), bundling the 2026-06-06 fix batch + the first external community contribution (library build, PR #185 by Danny Wilson). This is the baseline we branch the "CLI consumes the library" pivot from. Bumps Cargo 0.1.10 -> 0.2.0 (fork's own version line; upstream rtk v0.x tags are unrelated, hence the fork-namespaced tag). CHANGELOG [0.2.0] documents: - SECURITY: rtk-ai#2286 never-auto-allow + the live-path Ask fix. - FIXED: tsc/mypy/next lying-success filters. - PERF: grep/find 40->67%, rtk-ai#2289 decorator strip. - ADDED: library MVP (output_summary + lib.rs), with the dead-code caveat that the bin does not yet consume the lib (next-release work). council-reviewed (metadata-only, approved). No source changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thehoff
added a commit
to thehoff/contextcrawler
that referenced
this pull request
Jun 7, 2026
Multi-model audit (2 Claude security/bug agents + council) of the v0.2.0->v0.4.0 change found the rename/pivot security-sound (rtk-ai#2286 gate, fail-closed handling, dual-path gates all survived; verified empirically). Fixes for the findings: - MEDIUM: legacy CLAUDE.md managed-block recognised on upgrade. Pre-0.3.0 installs wrote <!-- rtk-instructions blocks; the new code only matched <!-- ctxcrl-instructions and APPENDED a duplicate (orphan on uninstall). Now recognises LEGACY_BLOCK_START/END as aliases for find/strip while always writing the canonical marker -> upgrade replaces in place, uninstall removes. - LOW: fixed the TimedExecution doctest (hidden stub fns) that the lib pivot newly activated (binary crates skip doctests; the lib runs them). - LOW: migrate .rtk/filters/*.toml subdir when .ctxcrl already exists (per-file, never overwrite). - LOW: symlink guard before renaming the project-local .rtk dir. Deferred (non-exploitable, tracked): narrow core from #[doc(hidden)] pub to pub(crate) (the visibility follow-up). council-reviewed; cargo test --doc now 25/0; 3054 tests pass (7 pre-existing hooks::integrity /tmp env). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thehoff
added a commit
to thehoff/contextcrawler
that referenced
this pull request
Jun 7, 2026
Detailed documentation pass for the renamed lib+bin contextcrawler. - README: corrected stale/removed-command claims (web/sessions/supply-chain check/security log were gone), added a Use-as-a-library section, source-only install, CTXCRL_*/RTK_* env note. Author-credit note kept verbatim. - docs/guide: new commands.md (per-ecosystem command/filter reference, 73 rule entries, honest savings figures), library.md (curated experimental API), architecture.md (lib+bin split, hook/gate flow + mermaid), getting-started rewrite (install/hook/paths/agents). - docs/usage/TRACKING: history.db (not tracking.db), ctxcrl paths, ctxcrl_cmd schema, inflation accounting; security/working-with-the-gate enriched with the rtk-ai#2286 model + supply-chain config. - cleanup: archived the finished rebrand-process docs; fixed stale rtk_cmd/ rtk_equivalent identifiers + a wrong ~/.config/contextcrawler trust path in src/cli.rs --global help (-> ctxcrl). House style: Aus English, no em-dashes, code-as-source-of-truth; origin attributions (rtk-ai/rtk, contextzip, Tirith) kept. council-reviewed across three passes (accuracy); final pass clean. 3075 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thehoff
added a commit
to thehoff/contextcrawler
that referenced
this pull request
Jun 12, 2026
…tk-ai#2286 follow-up) The permission gate blanket-downgraded ANY command with a command substitution to Ask, before the user's allowlist was even consulted — so `git -C "$(pwd)" status` prompted every time despite git/pwd being allowlisted. Empirically this was the dominant source of confirmation prompts. Replace the blanket rule with compositional attestation: a substitution is allowed to fall through to normal per-segment allow-matching ONLY when every payload is a side-effect-free, non-sensitive value producer (pwd/date/whoami/basename/dirname/uname/realpath/echo/printf/which/tty + git rev-parse|describe). Anything that can read file contents, hit the network, or mutate state still Asks. File-write redirects always Ask. Deny still pre-empts everything; malformed substitutions fail closed. Net (verified against a real allowlist + the live `hook claude` path): git -C "$(pwd)" status, echo "$(date)", git status $(whoami) -> silent curl ".../?d=$(cat secret)", echo "$(cat id_rsa)" -> Ask echo $(rm -rf /x) -> Deny ; > file -> Ask ; malformed -> Ask Security review by council (Codex + mmax). Both flagged the name-only whitelist bypasses — fixed before commit: - drop git branch/symbolic-ref (mutating variants) -> rev-parse/describe only - flag-guard `date` (-f/-r read a file, -s sets the clock) - drop seq (huge-output DoS) and hostname (bare arg mutates) Rejected: mmax's "|| not short-circuited" (Rust || does short-circuit). Added bypass regression tests for every flagged form + safe controls. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thehoff
added a commit
to thehoff/contextcrawler
that referenced
this pull request
Jun 12, 2026
The rtk-ai#2286 can't-attest Ask (command substitution / file-write redirect) has no escape hatch, so trusted overnight/headless workflows that legitimately read credentials, write files, or curl with substitution (structurally identical to the exfil shape the gate guards) hang on a prompt no one can answer. Add an opt-in env var CONTEXTCRAWLER_TRUST_UNATTESTABLE=1 (or "true") that skips ONLY the can't-attest Ask, letting such commands fall through to normal per-segment allow-matching and then the host's own permission mode. Off by default; safe-by-default is unchanged. Deny rules still fire regardless — trust never overrides a hard deny. The env read is split out (`unattestable_gate_trusted`) so the core `check_command_with_rules_trusted(.., trusted)` stays pure and testable with no env mutation. The rtk-ai#2286 Ask message now names the escape hatch so it's discoverable when hit. Mirrors the existing CONTEXTCRAWLER_TIRITH_DISABLED / CONTEXTCRAWLER_SUPPLY_CHAIN opt-outs. Tests: trust skips the Ask (sub + redirect), full allow-set reaches Allow, and deny still wins under trust. Co-Authored-By: Claude Opus 4.8 (1M context) <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.
Changes
.cursor/cli-config.json) and Gemini (.gemini/settings.json) with project-first lookup.execSyncwith safer alternative.decide_hook_action/HookDecisionflow so theDeferbranch is honored end-to-end.Tests
cargo fmt --all --check && cargo clippy --all-targets && cargo test --all— all passrtk rewriteandrtk hook copilotoutputs matches expected exit codes / JSON shapes