Conversation
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>) { |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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.
crates/rskim/src/cmd/agents.rs
Outdated
|
|
||
| /// 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; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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.
crates/rskim/src/cmd/agents.rs
Outdated
|
|
||
| let sessions = None; // Copilot CLI sessions are cloud-managed | ||
|
|
||
| let hooks = if detected { |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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), | |||
There was a problem hiding this comment.
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)" | ||
| )); |
There was a problem hiding this comment.
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`. | |||
There was a problem hiding this comment.
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
- ✅ Zero unsafe code - Entire diff uses safe Rust exclusively
- ✅ Error handling - All production code uses Result/? pattern, no unwrap() in business logic
- ✅ Security mindset - Multiple layers of defense (size guards, symlink checks, bounded I/O, rate limiting)
- ✅ Test coverage - 400+ tests passing with excellent behavior-focused integration tests
- ✅ Module decomposition - init.rs refactor from 1,047 lines into 5 focused modules is exemplary
- ✅ Strategy Pattern - HookProtocol and SessionProvider traits provide clean multi-agent abstraction
- ✅ 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 initdefaults to ClaudeCode (SAFE)skim rewrite --hookdefaults to ClaudeCode (SAFE)skim learnmigration 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:
- Hook warnings now route to ~/.cache/skim/hook.log (was stderr)
- skim_hook_version now reports actual version (was hardcoded "1.0.0")
- 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
- 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
Summary
check_hook_version_mismatch()fromeprintln!tohook_log::log_hook_warning(), preserving the zero-stderr invariant in hook modeprotocol_for_agent()factory so all 6 agents (Claude, Cursor, Gemini, Copilot, Codex, OpenCode) use their protocol'sparse_input()/format_response()instead of hardcoded Claude JSONskim agentsnow usesverify_script_integrity()for hook status (ok/tampered/missing/unknown)process::exit(0)to prevent agent hangsskim init --agent <name>with per-agent config dir resolution, script generation, and agent-aware uninstalldedup_invocations()by (command_key, timestamp) across agents--forcerequired if user modifiedTest plan
cargo test --all-features— all pass (28 new tests)cargo clippy -- -D warnings— zero warningscargo fmt -- --check— cleanecho '{"tool_input":{"command":"cargo test"}}' | skim rewrite --hook→ Claude JSON, empty stderrecho '{"tool_input":{"command":"cargo test"}}' | skim rewrite --hook --agent gemini→ Gemini JSON, empty stderrecho '{"command":"cargo test"}' | skim rewrite --hook --agent cursor→ Cursor JSON, empty stderrskim init --agent codex --yes→ awareness-only messageskim init --yes --dry-run→ Claude backward compatskim agents --json→ integrity field uses SHA-256