Skip to content

feat: wave/7 gap fixes — all 9 Shepherd audit items#94

Merged
dean0x merged 63 commits intomainfrom
wave/7
Mar 27, 2026
Merged

feat: wave/7 gap fixes — all 9 Shepherd audit items#94
dean0x merged 63 commits intomainfrom
wave/7

Conversation

@dean0x
Copy link
Copy Markdown
Owner

@dean0x dean0x commented Mar 25, 2026

Summary

  • P0: Zero-stderr fix — Redirect check_hook_version_mismatch() from eprintln! to hook_log::log_hook_warning(), preserving the zero-stderr invariant in hook mode
  • P0: HookProtocol dispatch — Wire protocol_for_agent() factory so all 6 agents (Claude, Cursor, Gemini, Copilot, Codex, OpenCode) use their protocol's parse_input()/format_response() instead of hardcoded Claude JSON
  • P1: SHA-256 integrityskim agents now uses verify_script_integrity() for hook status (ok/tampered/missing/unknown)
  • P1: Hook timeout — 5-second watchdog thread calls process::exit(0) to prevent agent hangs
  • P1: Collision detection — Init warns when other Bash PreToolUse hooks exist (informational, not blocking)
  • P1: Multi-agent initskim init --agent <name> with per-agent config dir resolution, script generation, and agent-aware uninstall
  • P2: Session dedupdedup_invocations() by (command_key, timestamp) across agents
  • P2: Awareness tracking — SHA-256 hash manifest for awareness files, --force required if user modified
  • P2: Agent-not-found — Helpful error with install hints when target agent isn't installed

Test plan

  • cargo test --all-features — all pass (28 new tests)
  • cargo clippy -- -D warnings — zero warnings
  • cargo fmt -- --check — clean
  • Manual: echo '{"tool_input":{"command":"cargo test"}}' | skim rewrite --hook → Claude JSON, empty stderr
  • Manual: echo '{"tool_input":{"command":"cargo test"}}' | skim rewrite --hook --agent gemini → Gemini JSON, empty stderr
  • Manual: echo '{"command":"cargo test"}' | skim rewrite --hook --agent cursor → Cursor JSON, empty stderr
  • Manual: skim init --agent codex --yes → awareness-only message
  • Manual: skim init --yes --dry-run → Claude backward compat
  • Manual: skim agents --json → integrity field uses SHA-256

Dean Sharon and others added 30 commits March 25, 2026 18:17
Pure refactor of the 1,047-line init.rs into a module directory:
  - init/mod.rs: public entry points (run, command)
  - init/flags.rs: InitFlags struct and parse_flags()
  - init/helpers.rs: resolve_config_dir, prompt helpers, check_mark, print_help
  - init/state.rs: DetectedState, detect_state, settings parsing
  - init/install.rs: run_install, hook script/settings patching
  - init/uninstall.rs: run_uninstall, remove_skim_from_settings

Zero behavior change. All existing tests pass.

Co-Authored-By: Claude <noreply@anthropic.com>
Add CodexCli, GeminiCli, CopilotCli, Cursor, OpenCode variants to
AgentKind with from_str parsing (including aliases), display_name,
cli_name, all_supported iterator, and rules_dir for per-agent file
conventions.

Update --agent error messages in discover.rs and learn.rs to use
dynamic agent list from AgentKind::all_supported().

13 new unit tests covering all AgentKind methods + round-trip.

Co-Authored-By: Claude <noreply@anthropic.com>
…se 0.3-0.4)

New hooks module with agent-agnostic HookProtocol trait defining:
  - parse_input: extract command from agent-specific JSON
  - format_response: build agent-specific response JSON
  - generate_script: create hook shell script
  - install/uninstall: stub methods for Phase 2 migration

Claude Code implementation extracts tool_input.command, formats
hookSpecificOutput with updatedInput, and generates skim-rewrite.sh
with --agent claude-code flag.

SECURITY: format_response never sets permissionDecision.

12 new unit tests covering trait, parsing, formatting, script gen.

Co-Authored-By: Claude <noreply@anthropic.com>
Update run_hook_mode to accept optional AgentKind parameter. When
--agent is specified with a non-Claude agent, hook mode passes through
(exit 0) until Phase 2 adds implementations.

Add --agent flag to clap Command definition and help text.
5 new unit tests for parse_agent_flag.

Co-Authored-By: Claude <noreply@anthropic.com>
Make skim learn agent-aware for rules file generation:
- Claude Code: .claude/rules/skim-corrections.md (no frontmatter)
- Cursor: .cursor/rules/skim-corrections.mdc (alwaysApply frontmatter)
- Copilot: .github/instructions/skim-corrections.instructions.md
- Single-file agents (Codex, Gemini, OpenCode): print to stdout
  with instructions to paste into agent config

Add rules_filename() helper for agent-specific file extensions.
Update error message for --agent to use dynamic agent list.

7 new unit tests for rules_filename and frontmatter generation.

Co-Authored-By: Claude <noreply@anthropic.com>
Add serde_yaml_ng workspace dependency for future YAML config parsing.

Co-Authored-By: Claude <noreply@anthropic.com>
…wave/7 phase 0.7)

Commands starting with "skim " are already hook-rewritten and should
not be counted as "missed optimizations" in discover's analysis.

Also update --agent error message to use dynamic agent list.

2 new unit tests for the skim command exclusion filter.

Co-Authored-By: Claude <noreply@anthropic.com>
- Fix hardcoded version "1.0.0" in suggest output, use env!("CARGO_PKG_VERSION")
- Fix flaky test_hook_version_mismatch_warning by isolating HOME to temp dir
- Fix test_learn_generate_writes_file expecting old filename cli-corrections.md
- Update stale doc comment and help text for learn --generate
- Centralize duplicated HOOK_SCRIPT_NAME/SETTINGS_FILE/SETTINGS_BACKUP constants
- DRY: extract AgentKind::parse_cli_arg to replace duplicated error formatting
- Remove dead duplicate if/else branches in install.rs run_install
- Fix rustfmt formatting issues
Implement CodexCliProvider for parsing Codex CLI event-stream JSONL
sessions from ~/.codex/sessions/ (YYYY/MM/DD/rollout-*.jsonl). Correlates
codex.tool_decision events with codex.tool_result events by tool_decision_id.

Add CodexCliHook as awareness-only HookProtocol implementation since Codex
CLI has no PreToolUse hook equivalent. All hook methods return no-ops.

- Session provider: detect, find_sessions (date-dir walk), parse_session
- Security: 100MB file size limit, symlink traversal guard, graceful degradation
- Hook: HookSupport::AwarenessOnly, all methods return None/Null/empty
- Registration: added to detect_agents() and hooks module
- Tests: 17 unit tests (10 session + 7 hook), test fixture included
- All 1341 tests pass, clippy clean

Co-Authored-By: Claude <noreply@anthropic.com>
Add GeminiCli agent support to the multi-agent session infrastructure:

Session Provider (session/gemini.rs):
- GeminiCliProvider with SKIM_GEMINI_DIR env override for testability
- Dual format detection: JSON array (legacy) vs JSONL (current)
- Tool name mapping: shell/bash -> Bash, read_file -> Read, etc.
- 100MB file size guard and symlink traversal protection
- 11 unit tests covering both formats, correlation, edge cases

Hook Protocol (hooks/mod.rs + hooks/gemini.rs):
- HookProtocol trait with parse_input, format_response, generate_script
- GeminiCliHook implementing BeforeTool event contract
- Absolute binary path in generated scripts (GRANITE #685 lesson)
- 8 unit tests for hook behavior, script generation, input parsing

Registration:
- AgentKind::GeminiCli variant with from_str and display_name
- GeminiCliProvider registered in detect_agents()
- Fix cli_discover::test_discover_no_agent_dir to neutralize all providers

Co-Authored-By: Claude <noreply@anthropic.com>
Part 1 - Session Provider (session/copilot.rs):
- CopilotCliProvider with SKIM_COPILOT_DIR env override
- YAML header parsing (serde_yaml_ng) + JSONL body parsing
- Tool name mapping: bash -> Bash, readFile -> Read, etc.
- Result correlation by toolUseId -> id
- 100MB max session size guard, symlink traversal protection
- 16 unit tests covering all parsing paths

Part 2 - Hook Protocol (hooks/copilot.rs):
- CopilotCliHook implementing deny-with-suggestion pattern
- Copilot's allow+updatedInput is broken; deny with reason works
- format_response emits {permissionDecision: "deny", reason: "..."}
- Upgrade path documented: change format_response when allow ships
- 11 unit tests including deny-is-not-allow assertions

Registration:
- session/mod.rs: CopilotCliProvider added to detect_agents()
- hooks/mod.rs: copilot module exported

Co-Authored-By: Claude <noreply@anthropic.com>
Implement CursorProvider (session/cursor.rs) that reads Cursor's
state.vscdb SQLite database with read-only access, 1s busy timeout,
and LIMIT-bounded queries for safe access to large databases.
JSON parsing layer maps Cursor tool names (run_terminal_command,
read_file, write_file, edit_file) to normalized ToolInput variants.

Implement CursorHook (hooks/cursor.rs) with real hook support for
Cursor's beforeShellExecution event format (command at top level,
permission/updated_input response schema).

Register both modules in detect_agents() and hooks/mod.rs. Fix
cli_discover test isolation by disabling Cursor provider env to
prevent real Cursor installations from affecting test assertions.

29 new tests (17 session + 12 hook), all passing.

Co-Authored-By: Claude <noreply@anthropic.com>
Implement OpenCodeProvider for reading OpenCode's SQLite session database:
- Walk-up detection from cwd for .opencode/ directory
- SKIM_OPENCODE_DIR env override for testability
- SQLite read-only access with busy_timeout(1000)
- Tool call JSON parsing with result correlation by tool_call_id
- Tool name mapping: bash/shell, read_file, write_file, edit_file, etc.

Add awareness-only OpenCodeHook (no real hook mechanism):
- HookSupport::AwarenessOnly — OpenCode uses TypeScript plugins
- All HookProtocol methods are no-ops

Register both in session/mod.rs and hooks/mod.rs.

22 new tests covering JSON parsing, tool mapping, result correlation,
graceful degradation for malformed data, and hook awareness behavior.

Co-Authored-By: Claude <noreply@anthropic.com>
Define the custom filter rule system for .skim.toml, covering:
- Four filter actions (remove, collapse, keep, replace)
- Match criteria (pattern regex, node_type, language, mode)
- Priority-based conflict resolution chain
- Trust model and security considerations
- skim verify command spec with validation checks
- Pipeline integration points (post-transform, pre-truncation)
- Inline test examples for each action type
- Rust type reference for implementors

Co-Authored-By: Claude <noreply@anthropic.com>
Implement the `skim agents` subcommand that detects installed AI coding
agents and reports their session paths, hook installation status, and
rules directory presence.

Supported agents: Claude Code, Cursor, Codex CLI, Gemini CLI, Copilot CLI.

- Add AgentKind variants (Cursor, CodexCli, GeminiCli, CopilotCli)
  with cli_name() and all_supported() methods
- Detect each agent via filesystem probing (session dirs, config files)
- Report hook status (installed/not installed/not supported)
- Support --json flag for machine-readable output
- Register in KNOWN_SUBCOMMANDS, dispatch, and completions
- Add 12 unit tests and 8 integration tests

Co-Authored-By: Claude <noreply@anthropic.com>
Add tamper detection for skim hook scripts using SHA-256 hashing.
Each agent's hook gets a companion .sha256 manifest file. Changes
applied to the split init module structure (wave/7 Phase 0).

New modules:
- integrity.rs: hash computation, manifest read/write, verification
- hook_log.rs: file-based logging (NEVER stderr) with 1MB rotation

Behavior matrix:
- Install/upgrade: compute and store hash manifest
- Uninstall: check integrity, require --force if tampered
- Hook execution: log-only warning (never stderr per GRANITE #361)
- Integrity warning subsumes version mismatch check

Init changes (split module):
- flags.rs: add --force field
- mod.rs: add --force to clap Command
- install.rs: compute hash after atomic script write
- uninstall.rs: integrity check + --force gate + manifest cleanup
- helpers.rs: add --force to help text

Rewrite changes:
- check_hook_integrity() with per-agent daily rate limiting
- resolve_agent_name() and resolve_hook_config_dir() helpers
- Per-agent version mismatch stamps (.hook-version-warned-{agent})
- SKIM_CACHE_DIR env override for test isolation

Co-Authored-By: Claude <noreply@anthropic.com>
Add 27 new integration tests covering cross-agent validation:

- Cross-agent discover: simultaneous Claude Code + Codex detection,
  --agent filter correctly excludes other agents' sessions
- Cross-agent learn: per-agent rules file format verification for
  Claude Code (.md), Cursor (dry-run), Copilot (.instructions.md
  with applyTo frontmatter), and Codex (stdout, no file written)
- Privacy: no cross-agent data leakage (filtering by codex does not
  include Claude Code error patterns)
- Hook protocol: --hook --agent <name> tests for claude-code (full
  hookSpecificOutput), gemini/copilot/cursor (passthrough), and
  unknown agent (graceful handling)
- Stderr cleanliness: hook mode produces ZERO stderr for all agents
  and passthrough scenarios
- Discover accuracy: skim-prefixed commands excluded from "missed
  optimization" command counts
- Agents command: JSON has 6 entries, session count accuracy,
  OpenCode shows "TypeScript plugin model" for hooks
- Init multi-agent: help text mentions Claude Code, rewrite help
  mentions --agent flag

Also applies rustfmt formatting fixes to source files.

Total test count: 1522 (up from 1495).

Co-Authored-By: Claude <noreply@anthropic.com>
check_hook_version_mismatch() was using eprintln! in hook mode,
violating the zero-stderr invariant (GRANITE #361 Bug 3). Now uses
hook_log::log_hook_warning() to write to ~/.cache/skim/hook.log instead.

Updated test to assert stderr is empty and verify warning goes to
hook.log file.

Co-Authored-By: Claude <noreply@anthropic.com>
Replace hardcoded Claude Code logic in run_hook_mode() with protocol
dispatch via protocol_for_agent() factory. Each agent now uses its own
parse_input/format_response implementations:

- Claude Code: hookSpecificOutput.updatedInput (unchanged behavior)
- Cursor: permission=allow, updated_input.command
- Gemini: decision=allow, tool_input.command
- Copilot: permissionDecision=deny with suggestion in reason
- Codex/OpenCode: AwarenessOnly passthrough (empty stdout, exit 0)

Removed unused HookResponse/HookSpecificOutput/UpdatedInput structs.
Normalized Gemini import path to crate::cmd::session::AgentKind.
Removed dead_code attributes from actively dispatched types.

Updated E2E tests: agent-specific JSON assertions for match cases,
passthrough for no-match, zero-stderr verification for all 6 agents.

Co-Authored-By: Claude <noreply@anthropic.com>
detect_claude_hook() was using simple is_file() check instead of
verify_script_integrity(). Now reports "ok", "tampered", "missing",
or "unknown" based on SHA-256 hash verification against stored manifest.

Added HookStatus Debug derive and 3 unit tests covering all integrity
status values: ok (valid hash), tampered (modified script), missing
(absent script file).

Co-Authored-By: Claude <noreply@anthropic.com>
Spawn a watchdog thread at the start of run_hook_mode() that sleeps
for HOOK_TIMEOUT_SECS (5s) and then exits cleanly if processing hasn't
completed. Prevents slow hook processing from hanging the agent.

On timeout: logs warning to hook.log, exits 0 (passthrough — agent
sees empty stdout, same as no match). No stderr output.

Added HOOK_TIMEOUT_SECS constant and structural tests.

Co-Authored-By: Claude <noreply@anthropic.com>
skim init now scans settings.json for existing non-skim Bash PreToolUse
hooks and warns the user about potential collisions. This is
informational only (not a blocker) — both hooks will fire but the
second is typically a no-op.

Added existing_bash_hooks field to DetectedState, scan_existing_bash_hooks()
function, warning output in run_install(), and 4 unit tests.

Co-Authored-By: Claude <noreply@anthropic.com>
Add --agent <name> flag to skim init for installing hooks to non-Claude
agents. Supports all 6 agents: claude-code (default), cursor, gemini,
copilot, codex, opencode.

Changes:
- flags.rs: Add agent field to InitFlags, parse --agent with validation
- helpers.rs: Add resolve_config_dir_for_agent() for agent-specific
  config directory resolution (platform-aware for Cursor macOS/Linux)
- state.rs: Use agent-aware config dir in detect_state()
- install.rs: Dynamic header text using agent display name, preserve
  agent through scope-override flags
- mod.rs: Add --agent to clap Command definition
- helpers.rs: Update help text with --agent documentation and examples
- cli_init.rs: Update help test for --agent flag

6 unit tests for flag parsing: default agent, cursor, gemini, unknown
error, missing value error, backward compatibility.

Co-Authored-By: Claude <noreply@anthropic.com>
…rror

Fix 7 - Session deduplication:
  collect_invocations() now deduplicates by (input_key, timestamp) across
  agents, preventing double-counting when multiple agents observe the same
  command. Added dedup_invocations() with 5 unit tests.

Fix 8 - Awareness file uninstall tracking:
  Added write_awareness_hash() and verify_awareness_integrity() helpers
  in integrity.rs for tracking generated awareness files via SHA-256.
  Uses "{agent}-awareness" key pattern. Added 2 round-trip tests.

Fix 9 - Agent-not-found error:
  verify_agent_installed() checks that the target agent's config directory
  exists before proceeding with installation. Returns a clear error with
  install hints for each agent when not found. Skips check for Claude Code
  (always proceed) and --project mode (we create the dir).

Co-Authored-By: Claude <noreply@anthropic.com>
P0 fixes:
- Uninstall now respects --agent flag instead of hardcoding ClaudeCode
  (uninstall.rs: resolve_config_dir_for_agent)
- Integrity hash operations use flags.agent.cli_name() instead of
  hardcoded "claude-code" in install.rs and uninstall.rs
- Hook script generation includes --agent flag for non-ClaudeCode agents
- Dual-scope check uses agent-aware config dir resolution

P1 fixes:
- Add agent_cli_name field to DetectedState for agent-aware operations
- Remove unused resolve_config_dir wrapper (dead code after refactor)
- Fix hardcoded "Claude config:" label to generic "Config:" in install
- Extract emit_rewrite_result() to eliminate 3x repeated pattern
- Refactor gemini hook tests to use shared hook() helper
- Simplify iterator chain in detect_all_agents
///
/// When multiple agents observe the same command at the same time,
/// only the first occurrence is retained. Order is preserved.
fn dedup_invocations(invocations: &mut Vec<ToolInvocation>) {
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Performance: Dedup allocates 2 strings per invocation unnecessarily (85% confidence)

dedup_invocations creates a format!("bash:{command}") String and clones a timestamp String for every invocation just to use as a HashSet key. For large session histories with thousands of invocations, this creates 2 heap allocations per invocation.

When only one provider is active (the common case), deduplication is unnecessary since cross-agent duplicates are impossible.

Fix: Short-circuit dedup for single-provider case:

if providers.len() > 1 {
    dedup_invocations(&mut all_invocations);
}

/// Priority: `SKIM_CACHE_DIR` env > `dirs::cache_dir()/skim`.
/// The env override enables test isolation on all platforms (especially macOS
/// where `dirs::cache_dir()` ignores `$XDG_CACHE_HOME`).
fn cache_dir() -> Option<std::path::PathBuf> {
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Consistency: cache_dir() duplicated across two modules (92% confidence)

The cache_dir() function is identically duplicated in rewrite.rs:1307 and hook_log.rs:79. Both check SKIM_CACHE_DIR env then fall back to dirs::cache_dir()/skim.

This violates the DRY pattern seen elsewhere where shared helpers are centralized (e.g., has_skim_hook_entry is in init/state.rs and re-exported).

Fix: Make the hook_log.rs version pub(super) and import it from rewrite.rs, or extract to cmd/mod.rs.


/// Maximum settings.json size we'll read (10 MiB), consistent with
/// the guard in `init/state.rs`.
const MAX_SETTINGS_SIZE: u64 = 10 * 1024 * 1024;
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Consistency: MAX_SETTINGS_SIZE duplicated in two modules (90% confidence)

MAX_SETTINGS_SIZE: u64 = 10 * 1024 * 1024 is defined independently in agents.rs:364 and init/state.rs:10. The agents.rs doc comment even states "consistent with the guard in init/state.rs" yet defines its own copy.

Within init/, helpers.rs correctly references super::state::MAX_SETTINGS_SIZE, showing the project pattern for sharing constants.

Fix: Widen init/state.rs::MAX_SETTINGS_SIZE from pub(super) to pub(crate) and import it in agents.rs.

} else {
false
};
// "0 failed" is a success indicator in test output — exclude it
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Complexity: looks_like_error has 10 boolean conditions in final expression (85% confidence)

The return statement chains 10 OR-conditions including nested has_error (6 sub-conditions) and has_failed (with negated inner condition). Each check is simple but the composite requires careful reading. Adding new patterns is error-prone.

Fix: Refactor into named predicates or data-driven pattern table:

const ERROR_PATTERNS: &[&str] = &[
    "not found", "no such file", "permission denied",
    "command not found",
];

fn looks_like_error(content: &str) -> bool {
    let check = truncate_utf8(content, 1024);
    let lower = check.to_lowercase();
    has_error_prefix(&lower)
        || has_non_zero_failure(&lower)
        || ERROR_PATTERNS.iter().any(|p| lower.contains(p))
}```

fn run_hook_mode() -> anyhow::Result<ExitCode> {
// A2: Version mismatch check — rate-limited daily warning
check_hook_version_mismatch();
fn run_hook_mode(agent: Option<AgentKind>) -> anyhow::Result<ExitCode> {
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Complexity: run_hook_mode mixes 6 responsibilities (85% confidence)

This 116-line function handles: watchdog spawning, protocol dispatch, integrity checks, version checks, stdin reading, JSON parsing, command extraction, compound detection, rewriting, and response formatting. It has 6 return Ok(ExitCode::SUCCESS) exit paths and complex nesting.

Fix: Extract into phases:

fn run_hook_mode(agent: Option<AgentKind>) -> anyhow::Result<ExitCode> {
    spawn_watchdog();
    let (agent_kind, protocol) = resolve_protocol(agent)?;
    run_pre_hook_checks(agent_kind);
    let command = read_hook_command(&protocol)?;
    let rewritten = try_rewrite_command(&command);
    emit_hook_response(&protocol, &command, rewritten)
}```

.unwrap_or("{}");

let arguments: serde_json::Value =
serde_json::from_str(arguments_str).unwrap_or_default();
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Rust: unwrap_or_default() silently swallows JSON parse errors (82% confidence)

serde_json::from_str(arguments_str).unwrap_or_default() silently creates Value::Null if JSON is malformed. If a tool call has corrupted arguments, the invocation will have empty fields, producing confusing results in discover/learn analysis.

Fix: Skip tool calls with malformed arguments:

let arguments: serde_json::Value = match serde_json::from_str(arguments_str) {
    Ok(v) => v,
    Err(_) => continue, // skip tool calls with malformed arguments
};

}

/// Factory: create the appropriate HookProtocol implementation for a given agent.
pub(crate) fn protocol_for_agent(kind: AgentKind) -> Box<dyn HookProtocol> {
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Rust: Blanket #[allow(dead_code)] masks actual dead code (85% confidence)

The #[allow(dead_code)] annotation covers the entire HookProtocol trait with a comment explaining which methods are test-only vs production. This blanket suppression masks truly unused methods from compiler analysis.

The methods parse_input, format_response, and hook_support are called in production via protocol_for_agent() -- they are NOT dead code. Only agent_kind, generate_script, install, and uninstall are test-only.

Fix: Move #[allow(dead_code)] to individual methods that are genuinely only called in tests.


let sessions = None; // Copilot CLI sessions are cloud-managed

let hooks = if detected {
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Performance: Copilot hooks directory scan has no bound (83% confidence)

To detect whether skim is installed for Copilot, the code reads every .json file in .github/hooks/ without limit. There's no bound on the number of files scanned. A repository with many hook files could cause unbounded file I/O in the agents subcommand.

Fix: Add a bound and short-circuit on first match:

let has_skim_hook = std::fs::read_dir(hooks_dir).ok().is_some_and(|entries| {
    entries.flatten().take(50).any(|e| {
        e.path().extension().is_some_and(|ext| ext == "json")
            && std::fs::read_to_string(e.path())
                .ok()
                .is_some_and(|c| c.contains("skim"))
    })
});


#[test]
fn test_agents_json_has_six_entries() {
let output = skim_cmd().args(["agents", "--json"]).output().unwrap();
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Tests: Hardcoded agent count assertion is fragile (90% confidence)

test_agents_json_has_six_entries asserts agents.len() == 6 with a magic number. When a 7th agent is added, this test fails with a cryptic error. The assertion couples the test to a count rather than testing meaningful behavior.

Furthermore, test_agents_lists_all_supported (line 131) already asserts the presence of all 6 agent names, making this count-only test redundant.

Fix: Either remove this test entirely (the existing test provides stronger coverage) or make it self-documenting:

let expected = ["claude-code", "cursor", "codex", "gemini", "copilot", "opencode"];
assert_eq!(agents.len(), expected.len(), "unexpected agent count change");

@@ -1261,7 +1339,7 @@ fn print_suggest(original: &str, result: Option<(&str, RewriteCategory)>, compou
category: result.map(|(_, c)| c),
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Regression: skim_hook_version field changed from hardcoded to dynamic (85% confidence)

The skim_hook_version field in --suggest JSON output was hardcoded to "1.0.0" and is now env!("CARGO_PKG_VERSION"). This is arguably a bug fix (the hardcoded version was wrong), but it is a behavioral change in JSON output.

Action: This is correct behavior. Requires explicit sign-off that this is an intentional bug fix. Document in release notes.

// Emit warning to hook log (NEVER stderr -- GRANITE #361 Bug 3)
super::hook_log::log_hook_warning(&format!(
"version mismatch: hook script v{hook_version}, binary v{compiled_version} (run `skim init --yes` to update)"
));
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Regression: Stamp file path changed -- orphaned legacy file persists (82% confidence)

The version mismatch stamp file path changed from .hook-version-warned to .hook-version-warned-{agent_name}. Existing users who upgrade will have an orphaned .hook-version-warned stamp file that is never read or cleaned up.

This is not a functional regression (users get one extra warning on upgrade day), but the old file persists indefinitely.

Fix: Add one-time cleanup of the legacy stamp file:

// In check_hook_version_mismatch(), after computing stamp_path:
let legacy_stamp = stamp_path.with_file_name(".hook-version-warned");
let _ = std::fs::remove_file(&legacy_stamp);

@@ -0,0 +1,545 @@
//! Install flow for `skim init`.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Wave/7 Code Review Summary

Reviewed 10 reports across 42 changed files (+10,768 / -1,244 lines). This PR introduces multi-agent support (6 agents) with session providers, hook protocols, integrity verification, and shell redirect support.

Overall Assessment

APPROVED WITH CONDITIONS - All findings are non-blocking (no CRITICAL issues). The architecture is sound, test coverage is comprehensive, and the refactoring of the monolithic 1,047-line init.rs into focused sub-modules is exemplary.

Review Findings Consolidated

Blocking Issues (≥80% confidence):

  • 17 inline comments created covering high-confidence findings across security, architecture, complexity, performance, consistency, and regression categories
  • All findings are addressed with concrete, actionable fixes

High-Confidence Issue Counts:

Category Issues Confidence
Security 3 82-85%
Architecture 4 85-92%
Performance 2 83-85%
Complexity 3 85-92%
Consistency 2 90-92%
Rust 3 80-85%
Tests 1 90%
Regression 2 82-85%

Key Strengths

  1. Zero unsafe code - Entire diff uses safe Rust exclusively
  2. Error handling - All production code uses Result/? pattern, no unwrap() in business logic
  3. Security mindset - Multiple layers of defense (size guards, symlink checks, bounded I/O, rate limiting)
  4. Test coverage - 400+ tests passing with excellent behavior-focused integration tests
  5. Module decomposition - init.rs refactor from 1,047 lines into 5 focused modules is exemplary
  6. Strategy Pattern - HookProtocol and SessionProvider traits provide clean multi-agent abstraction
  7. Performance - Meets <50ms target (14.6ms for 3000-line files, 3x faster than target)

Lower-Confidence Issues (60-79%)

The following findings from review reports are below the 80% threshold for inline comments but should be noted:

  • Hash manifest written without restricted permissions (68%) - consider chmod 0600 on .sha256 files
  • Copilot YAML deserialization without limits (62%) - serde_yaml_ng may lack DoS protections
  • Agent CLI name unsanitized in path construction (65%) - add debug_assert on path separators
  • Levenshtein watchdog thread spawns on every invocation (70%) - consider lazy spawn after stdin read
  • Multiple test edge cases (65-70%) - provider neutralization duplication, test naming precision, rotation testing gaps

Backward Compatibility

All major regressions verified as safe:

  • skim init defaults to ClaudeCode (SAFE)
  • skim rewrite --hook defaults to ClaudeCode (SAFE)
  • skim learn migration logic included (cli-corrections.md → skim-corrections.md)
  • Session dedup is additive (SAFE for single-agent deployments)

Note: Three behavioral changes require release notes documentation:

  1. Hook warnings now route to ~/.cache/skim/hook.log (was stderr)
  2. skim_hook_version now reports actual version (was hardcoded "1.0.0")
  3. Rules filename changed to skim-corrections.md

Recommendation

Merge with condition: Address the 17 inline comments (concrete fixes provided for each). These are mechanical improvements that strengthen code quality without architectural changes. None block merge.


Review: Security (8/10), Architecture (7/10), Rust (8/10), Performance (8/10), Complexity (6/10), Consistency (8/10), Tests (8/10), Regression (8/10)

Generated by Claude Code review agent

Dean Sharon and others added 13 commits March 26, 2026 14:36
- Use to_ascii_lowercase() instead of to_lowercase() in looks_like_error
  to avoid Unicode-aware String allocation (all patterns are ASCII)
- Extract sanitize_for_rules() to deduplicate identical transformation
  logic between sanitize_error_output and sanitize_command_for_rules
- Show resolved rules file path in hint message instead of vague
  "agent-specific rules file" text

Co-Authored-By: Claude <noreply@anthropic.com>
- Update stale module doc to list all 6 providers (DOC-2)
- Restore SessionProvider trait doc comment lost during diff (DOC-6)
- Restore detect_agents() doc comment about env-var overrides (DOC-7)
- Short-circuit dedup when single provider is active (PERF-1)
- Use serde_json::to_string for canonical dedup key (RUST-5)

Co-Authored-By: Claude <noreply@anthropic.com>
- Rename test_learn_generate_cursor_dry_run_has_frontmatter to
  test_learn_generate_default_dry_run_preview (test uses default agent,
  not Cursor)
- Replace // ---- X ---- separators in discover.rs with
  // ============ banner style matching all other new files

Co-Authored-By: Claude <noreply@anthropic.com>
SEC-1: Add shell-safe path validation before interpolating binary_path
into generated bash hook scripts. Rejects characters that can escape
double-quote context (", `, $, \, newline, null byte) to prevent
shell injection via adversarial current_exe() paths.

SEC-2: Add MAX_SESSION_SIZE (500 MB) guard to OpenCode session provider's
parse_session(), consistent with all other providers. SQLite limit is
higher than the 100 MB JSON-provider limit due to database overhead.

Co-Authored-By: Claude <noreply@anthropic.com>
…roviders

SEC-3: Add MAX_DB_SIZE guard to Cursor's parse_session, matching the
file-size check pattern used by all other session providers (Claude,
Codex, Gemini, Copilot).

CMPLX-5: Extract process_cursor_tool_calls() helper from
parse_cursor_json_value() to reduce nesting depth from 5 to 3 and
improve readability of tool-call processing.

PERF-5: Replace split_yaml_header (which allocated a SessionMetadata
struct only to discard it) with skip_yaml_header that performs a
delimiter-only scan, avoiding serde_yaml deserialization overhead.

Co-Authored-By: Claude <noreply@anthropic.com>
- Replace magic number 6 with EXPECTED_AGENTS constant in agents count test
- Add stdout assertion for unknown agent fallback to Claude Code format
- Add log_hook_warning integration test verifying rotation at >1MB
- Add discover edge-case tests for --since and --agent missing values

Co-Authored-By: Claude <noreply@anthropic.com>
- SEC-4/PERF-2: Add size guard to detect_gemini_cli (read_settings_guarded)
  and detect_copilot_cli (.take(50) + per-file size check)
- CMPLX-4: Extract has_skim_hook_in_settings() helper to flatten 7-level
  chained nesting in detect_gemini_cli
- RUST-3: Call dirs::home_dir() once in detect_all_agents and thread
  through detect_agent, eliminating 4 redundant syscalls
- CONS-2: Remove duplicated MAX_SETTINGS_SIZE constant; widen
  init/state.rs visibility to pub(crate) and import in agents.rs

Co-Authored-By: Claude <noreply@anthropic.com>
- DOC-1: Module-level doc comment now references HookProtocol and
  agent-specific format_response() instead of Claude Code only
- DOC-4: SECURITY INVARIANT comment scoped to per-agent behavior
  (Claude Code never sets permissionDecision; Copilot uses deny)
- DOC-4: Help text updated to reflect multi-agent hook mode
- SEC-5: Audit log now rotates (shift scheme: .3 deleted, .2->.3,
  .1->.2, current->.1) instead of truncating to zero, matching
  the same pattern used in hook_log.rs

Co-Authored-By: Claude <noreply@anthropic.com>
…ove consistency

- Add default install()/uninstall() on HookProtocol trait, removing 6
  identical no-op stubs across all agent implementations
- Extract parse_tool_input_command() shared helper in mod.rs, used by
  claude, copilot, and gemini (cursor differs, codex/opencode are no-ops)
- Move #[allow(dead_code)] from trait-level to individual methods that
  are test-only (agent_kind, generate_script, install, uninstall)
- Add debug_assert! on version safety in all generate_script() impls
- Fix claude generate_script comment to include --agent claude-code
- Add doc comments on ClaudeCodeHook, CursorHook, CopilotCliHook,
  CodexCliHook structs
- Add standard test section separator to gemini.rs

Co-Authored-By: Claude <noreply@anthropic.com>
…icate cache_dir

- CONS-3/REG-6: parse_agent_flag now logs a warning via hook_log for
  unknown --agent values instead of silently ignoring them (preserves
  zero-stderr invariant for hook mode)
- ARCH-6: add TODO comment documenting that integrity checks are
  currently Claude Code-only and should extend to other RealHook agents
- CONS-1: make hook_log::cache_dir pub(super) and delegate from
  rewrite.rs to eliminate identical duplication

Co-Authored-By: Claude <noreply@anthropic.com>
Single source of truth for agent filesystem paths via AgentKind methods:
- dot_dir_name(), config_dir(), project_dir(), detect_dir(), rules_filename()

Split 1011-line agents.rs into focused modules:
- types.rs: AgentStatus, SessionInfo, HookStatus, RulesInfo
- detection.rs: detect_all_agents, per-agent detection functions
- formatting.rs: text/JSON output, help text
- util.rs: tilde_path, file counting, directory sizing

Additional fixes from code review:
- Harden generate_hook_script against shell injection (assert on all params)
- Fix Cursor hook detection (was hardcoded NotSupported, now checks actual state)
- Restore rules_dir() to Option<&'static str> (zero-copy)
- Standardize MAX_SESSION_SIZE scoping across session providers
- Replace glob imports with explicit imports
@dean0x dean0x merged commit 8c7a9a6 into main Mar 27, 2026
4 of 5 checks passed
@dean0x dean0x deleted the wave/7 branch March 27, 2026 09:30
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