Skip to content

feat(permissions): optimize auto-mode classifier with in-CWD fast path and two-stage classification - #1098

Draft
Zilv1nas wants to merge 1 commit into
masterfrom
feature/auto-mode-optimizer
Draft

feat(permissions): optimize auto-mode classifier with in-CWD fast path and two-stage classification#1098
Zilv1nas wants to merge 1 commit into
masterfrom
feature/auto-mode-optimizer

Conversation

@Zilv1nas

Copy link
Copy Markdown
Contributor

Summary

Optimizes the auto-mode permission classifier to reduce unnecessary GPU calls on shared B2B inference infrastructure.

Changes

1. In-CWD File Edit Fast Path (src/extensions/permissions/index.ts)

When mode is auto and the tool is write or edit, checks if the target file path resolves within ctx.cwd. If yes, auto-approves without calling the LLM classifier. Protected paths are excluded:

  • .git/, .env, .env.*, .kimchi/, .claude/, shell config files (.bashrc, .zshrc, .profile)

These protected paths still go through the classifier even when inside cwd.

2. Expanded Allowlist Verification (src/extensions/permissions/index.ts)

Verified that isReadOnlyTool() and isReadOnlyBashCommand() checks already run before the mode === "auto" branch in the tool_call handler (lines ~953-956). This means read-only MCP tools (list_*, get_*, describe_*) and read-only bash commands already skip the classifier in auto mode. No code change needed — confirmed correct.

3. Two-Stage Classification (src/extensions/permissions/classifier.ts, src/extensions/permissions/types.ts)

Restructured classifyToolCall to use a two-stage approach:

  • Stage 1 (fast): Lightweight classifier call with a minimal prompt suffix asking for an immediate verdict, using maxTokens=64. If verdict is safe, returns immediately — no second call needed.
  • Stage 2 (full reasoning): Only runs when Stage 1 does not return safe. This is the existing full classifier call with retries and fallback model.
  • Both stages share the same model, auth, and input.
  • ClassifierResult gains an optional stage field (1 or 2) to track which stage produced the result.

Test Coverage

  • 13 new tests for in-CWD fast path (auto-approve within cwd, reject protected paths, reject outside cwd, reject in default mode, notification emission)
  • 6 new/updated tests for two-stage classification (Stage 1 safe skip, Stage 1 requires-confirmation fallthrough, Stage 1 parse failure fallthrough, Stage 1 error fallthrough, Stage 1 maxTokens verification, call count adjustments)
  • All 384 tests pass, build compiles clean

Checklist

  • Bug fix / new feature
  • Tests added/updated
  • Lint and typecheck pass

…h and two-stage classification

- Add in-CWD file edit fast path: write/edit calls targeting files within
  ctx.cwd are auto-approved without invoking the LLM classifier, saving
  GPU resources. Protected paths (.git/, .env, .kimchi/, .claude/, shell
  configs) still go through the classifier.

- Verify expanded allowlist: isReadOnlyTool() and isReadOnlyBashCommand()
  checks already run before the auto-mode classifier branch, so read-only
  MCP tools and read-only bash commands skip the classifier in auto mode.

- Implement two-stage classification: Stage 1 uses a minimal prompt suffix
  with maxTokens=64 for a fast verdict. If Stage 1 returns safe, Stage 2
  is skipped entirely. Stage 2 is the existing full reasoning call with
  retries and fallback. ClassifierResult gains an optional stage field.

- Add comprehensive tests for all three changes (384 tests pass, build clean).

Co-Authored-By: Kimchi <noreply@kimchi.dev>
@Zilv1nas Zilv1nas added the new feature Introduces a new feature label Aug 27, 2026
@kimchi-review

kimchi-review Bot commented Aug 27, 2026

Copy link
Copy Markdown

Kimchi Code Review

Property Value
Commit e36a5ee
Author @Zilv1nas
Files changed 0
Review status Completed
Comments 5 (1 critical, 2 info, 2 warning)
Duration 112s

Summary

📊 Review Score: 70/100 (overall code quality — 0 lowest, 100 highest)
⏱️ Estimated effort to review: 3/5 (1 = trivial, 5 = very complex)

🧪 Tests: yes — Extensive test coverage was added. classifier.test.ts now covers Stage 1 safe short-circuit, Stage 1 fallthrough on requires-confirmation/parse failure/error/aborted, Stage 2 retry semantics, fallback model interactions, and the maxTokens: 64 request option. index.test.ts covers in-CWD auto-approval for write/edit, protected path exclusions, relative paths, path traversal, mode gating, and notification emission. Existing tests were correctly updated for the extra Stage 1 call in retry/fallback scenarios.

🔒 Security concerns found: isInCwdFileEdit normalizes cwd to ${cwd}/ only when it lacks a trailing slash. When ctx.cwd is the filesystem root /, the normalized prefix remains /, so every absolute path satisfies resolved.startsWith('/') and can be auto-approved without classifier review. Protected-path regexes are also case-sensitive, which may be bypassed on case-insensitive filesystems (e.g. .ENV).

📝 Found 5 issue(s). See inline comments for details.

What to expect

Kimchi will analyze the changes in this pull request and post:

  • A summary of the overall changes
  • Inline comments on specific lines with findings categorized by issue type

The review typically completes within a few minutes. This comment will be updated once the review is ready.

Interact with Kimchi
  • @getkimchi review — re-trigger a full review on the latest commit
  • @getkimchi summary — regenerate the PR summary
  • @getkimchi ignore — skip this PR (no review will be posted)
  • Reply to any inline comment to ask follow-up questions or request clarification
Configuration

Reviews are configured by your organization admin.
Review instructions, excluded directories, and severity thresholds can be adjusted per repository in the Kimchi dashboard.


Powered by Kimchi — AI-powered code review by CAST AI

@kimchi-review kimchi-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📊 Review Score: 70/100 (overall code quality — 0 lowest, 100 highest)
⏱️ Estimated effort to review: 3/5 (1 = trivial, 5 = very complex)

🧪 Tests: yes — Extensive test coverage was added. classifier.test.ts now covers Stage 1 safe short-circuit, Stage 1 fallthrough on requires-confirmation/parse failure/error/aborted, Stage 2 retry semantics, fallback model interactions, and the maxTokens: 64 request option. index.test.ts covers in-CWD auto-approval for write/edit, protected path exclusions, relative paths, path traversal, mode gating, and notification emission. Existing tests were correctly updated for the extra Stage 1 call in retry/fallback scenarios.

🔒 Security concerns found: isInCwdFileEdit normalizes cwd to ${cwd}/ only when it lacks a trailing slash. When ctx.cwd is the filesystem root /, the normalized prefix remains /, so every absolute path satisfies resolved.startsWith('/') and can be auto-approved without classifier review. Protected-path regexes are also case-sensitive, which may be bypassed on case-insensitive filesystems (e.g. .ENV).

📝 Found 5 issue(s). See inline comments for details.

const filePath =
typeof input.file_path === "string" ? input.file_path : typeof input.path === "string" ? input.path : ""
if (!filePath) return false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚨🔒 Security

isInCwdFileEdit builds normalizedCwd as ${cwd}/ only when cwd does not end with /. If ctx.cwd is exactly /, normalizedCwd is also /, and resolved.startsWith('/') is true for any absolute path. A malicious or mistaken write/edit targeting /etc/passwd or any other sensitive file outside the workspace would be auto-approved without reaching the classifier.

💡 Suggestion: Normalize cwd so the prefix check always requires a separator after the directory. Use const normalizedCwd = cwd === '/' ? cwd : cwd.replace(/\/+$/, '') and check resolved === normalizedCwd || resolved.startsWith(${normalizedCwd}/), or alternatively reject when resolved is the directory itself.

* fast path, even when they resolve within ctx.cwd. These paths can contain
* secrets, modify shell behaviour, or corrupt agent/harness state.
*/
const PROTECTED_PATH_PATTERNS: readonly RegExp[] = [

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️🔒 Security

PROTECTED_PATH_PATTERNS uses case-sensitive regexes such as /(^|\/)\.env$/. On case-insensitive filesystems an attacker or model could bypass the check with paths like .ENV or .Git/config, causing a protected file inside ctx.cwd to be auto-approved.

💡 Suggestion: Build or normalize the resolved path with a consistent casing and separator before matching, e.g. isProtectedPath(resolvedPath.toLowerCase()), and ensure the regex set is tested against common case variants.

const filePath =
typeof input.file_path === "string" ? input.file_path : typeof input.path === "string" ? input.path : ""
if (!filePath) return false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️🔧 Maintainability

The prefix check assumes POSIX / separators, but resolve from path uses the platform separator. On Windows, resolve returns backslash-separated paths while normalizedCwd may contain a trailing /, so resolved.startsWith(normalizedCwd) will always be false and the optimization is silently disabled.

💡 Suggestion: Normalize both resolved and cwd to POSIX-style separators with a helper before comparing, e.g. path.normalize(...).replace(/\\/g, '/').

return { ...stage1Result, stage: 1 }
}

// Stage 2 (full reasoning): the existing full classifier call with retries

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ℹ️🔧 Maintainability

The if (signal?.aborted) return unavailable("classifier aborted") check is duplicated immediately before the Stage 1 call. The first check at the top of classifyToolCall already returns in the same way, so the second check is dead code that can confuse readers.

💡 Suggestion: Remove the redundant signal?.aborted check on line 58, leaving only the initial guard.

// Stage 1 errors are non-fatal — fall through to Stage 2.
return { verdict: "requires-confirmation", reason: "stage 1 error, falling through", ok: false, retryable: false }
} finally {
clearTimeout(timeoutHandle)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ℹ️⚠️ Error Handling

runClassifierFast swallows Stage 1 exceptions and error/aborted stop reasons without logging the underlying cause. If Stage 1 starts consistently falling through in production, there is no diagnostic signal to explain why the optimization is not short-circuiting.

💡 Suggestion: Emit a debug or trace log in the catch block and in the error/aborted branches that includes the error message or stop reason, while still returning the non-fatal fallthrough result.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new feature Introduces a new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant