Skip to content

fix(security): port permission hardening from master + Copilot CLI adaptation#2286

Merged
aeppling merged 9 commits into
developfrom
fix/cve-permission-hardening-port
Jun 5, 2026
Merged

fix(security): port permission hardening from master + Copilot CLI adaptation#2286
aeppling merged 9 commits into
developfrom
fix/cve-permission-hardening-port

Conversation

@aeppling

@aeppling aeppling commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Changes

  • fix(permissions): port master's permission hardening commits to develop (lexer additions, verdict refinement).
  • fix(permissions): per-host config loaders for Cursor (.cursor/cli-config.json) and Gemini (.gemini/settings.json) with project-first lookup.
  • fix(permissions): redirect parsing fix.
  • fix(openclaw): replace execSync with safer alternative.
  • adapt(hook): route Copilot CLI handler through the new decide_hook_action / HookDecision flow so the Defer branch is honored end-to-end.

Tests

  • cargo fmt --all --check && cargo clippy --all-targets && cargo test --all — all pass
  • Copilot-CLI acceptance tests added (covers the same matrix the master tests cover for the other hosts)
  • Real-binary verification of rtk rewrite and rtk hook copilot outputs matches expected exit codes / JSON shapes

aeppling and others added 9 commits June 5, 2026 14:02
- 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.
@aeppling aeppling merged commit e1cd274 into develop Jun 5, 2026
17 checks passed
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>
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.

2 participants