Repository navigation
feat(init): chain tirith shell-hook install with fail-open detection - #2
Merged
Merged
Conversation
`contextcrawler init -g` is the documented one-shot install path. Tirith's
shell hook was a separate manual step (`eval "$(tirith init --shell …)"`
in the user's shell rc), which is easy to miss and leaves the defense-
in-depth gate disabled at the interactive shell layer.
This change chains Tirith hook installation into `init -g`:
1. Probe `which("tirith")`.
2. If found: detect shell from `$SHELL`, write a sentinel-wrapped
`eval "$(tirith init --shell <shell>)"` block to the user's rc file
(idempotent — re-runs detect the sentinel and skip).
3. If absent: print a fail-open notice with the install hint; the
ContextCrawler auto-rewrite path continues to work unchanged.
Design notes:
- Wrapped in `// ===== contextzip-downstream =====` sentinel comments so
upstream-rtk rebases stay confined to the import block plus this
single insertion in `run_default_mode`.
- Pure helpers (`detect_shell`, `shell_rc_path`, `rc_has_tirith_sentinel`,
`build_tirith_block`) are individually unit-testable; the integration
function `install_tirith_shell_hook` orchestrates them and does the
filesystem I/O behind a fail-open boundary.
- Supported shells: bash, zsh, fish. Unrecognized shells (nu, ksh, etc.)
print a SKIPPED notice with the user's $SHELL value.
- All I/O failures (read, create_dir_all, append) are silenced unless
-v is passed — init must not break because of an unwritable rc file.
Tests added (5):
* test_shell_rc_path_known_shells — bash/zsh/fish → expected paths
* test_shell_rc_path_unsupported_shell_is_none — nu/ksh/"" → None
* test_detect_shell_never_returns_empty — $SHELL contract
* test_rc_has_tirith_sentinel_detects_managed_block
* test_rc_has_tirith_sentinel_ignores_unrelated_content
* test_build_tirith_block_round_trips_through_sentinel_check
(re-running init detects its own sentinel and is a no-op)
Verified: cargo test hooks::init::tests → 75 passed / 0 failed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 14, 2026
This was referenced May 14, 2026
Merged
noogalabs
pushed a commit
to noogalabs/contextcrawler
that referenced
this pull request
Jun 4, 2026
…path G1 audit finding thehoff#2 of thehoff#100: the Tirith pre-execution gate and the supply-chain gate only ran on the legacy `contextcrawler rewrite` subcommand (hooks/rewrite_cmd.rs). The live Claude Code PreToolUse path (`contextcrawler hook claude` -> process_claude_payload) did permissions + rewrite but never invoked either gate, so the defence-in-depth layer was effectively dark for every real agent session. Wire both gates into process_claude_payload_with, after the existing PermissionVerdict::Deny handling and before get_rewritten: * New pure helper gate_decision(tirith, supply_chain) -> GateDecision { Proceed, Ask, Deny } maps the two gate verdicts. supply-chain Block -> Deny; supply-chain Unavailable -> Ask (fail closed); tirith should_downgrade -> Ask; both clean -> Proceed. Block outranks Ask. * supply-chain Block reuses the thehoff#102 deny machinery -- emits a PayloadAction::Deny (audit_tag deny:supply_chain_block) routed through the existing emit_claude_deny. No new PayloadAction variant. * A gate Ask sets gate_ask, which suppresses the permissionDecision "allow" so Claude Code prompts -- a gate Ask wins over a permissions Allow. * A gate-flagged command with no rewrite (e.g. `pip install foo`) cannot express "ask" in the PreToolUse protocol, so it fails CLOSED to a deny rather than a silent Skip. Both gates stay opt-in: Tirith via CONTEXTCRAWLER_TIRITH_REQUIRED, supply-chain via supply_chain.enabled. When disabled the gates are no-ops and the hook behaves byte-for-byte as before -- covered by a no-regression test asserting `git status` still rewrites with no deny. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
noogalabs
pushed a commit
to noogalabs/contextcrawler
that referenced
this pull request
Jun 4, 2026
…fixes (thehoff#111 G5) grep_cmd.rs (CRITICAL): the rg/grep argv placed pattern and path with no `--` separator, so a pattern shaped like `--pre=<cmd>` reached rg unscanned and ran an arbitrary preprocessor (RCE, thehoff#32). Restructure both the rg path and the plain-grep fallback as: fixed flags, then forbidden-checked extra_args, then `--`, then pattern, then path. Mirrors the thehoff#100 G5#1/thehoff#2 boundary already in ls.rs/tree.rs. tree.rs: confirmed `-o filename` is genuine native tree (v2.x) output-file flag — kept in VALUE_FLAGS with a clarifying comment; extracted the flag/path splitter into a testable helper. find_cmd.rs: a trailing slash on the search root made strip_prefix fail and emit absolute paths, breaking the relative-path contract. Normalise the walk root (strip trailing slash) before walking and stripping. Co-Authored-By: Claude Opus 4.7 (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.
Summary
`contextcrawler init -g` is the documented one-shot install. Tirith's shell hook used to be a separate manual step (`eval "$(tirith init --shell ...)"` in the user's shell rc), which is easy to miss and leaves the defense-in-depth gate disabled at the interactive shell layer.
This PR chains the Tirith hook installation into `init -g` with fail-open semantics — if the `tirith` binary is missing, init prints a hint and continues without it.
Behavior
Supported shells: bash, zsh, fish. Unrecognized shells (nu, ksh, etc.) get a SKIPPED notice with the detected `$SHELL` value — no rc file is touched.
Sample output
With Tirith installed:
```
Tirith shell hook: appended to /home/thehoff/.bashrc (restart your shell to activate)
```
Without Tirith:
```
Tirith shell hook: SKIPPED (binary not on PATH — fail-open)
Install with: cargo install tirith
Then re-run: contextcrawler init -g
```
Re-run (idempotent):
```
Tirith shell hook: already present in /home/thehoff/.bashrc
```
Design
Test plan
Independence from PR #1
This branch is off `develop` (clean, doesn't include #1's rebrand). No conflicts expected when both merge — they touch disjoint surfaces.
🤖 Generated with Claude Code