-
Notifications
You must be signed in to change notification settings - Fork 3.1k
feat(cli): protect CLI config files from silent edits #7573
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
6e3d227
feat(cli): add config path detection for permission protection
alex-alecu 2455a43
feat(cli): force permission prompt for config file edits
alex-alecu 21902b1
feat(cli): hide 'Always allow' in TUI for config file edits
alex-alecu c2969d2
fix(cli): close absolute-path bypass and extract DISABLE_ALWAYS_KEY c…
alex-alecu ed03c64
Merge branch 'main' into feat/config-permission-protection
alex-alecu 6105e35
fix(cli): detect nested config dirs in isRelative
alex-alecu a215147
fix(cli): check movePath in apply_patch config guard
alex-alecu 2dba685
refactor(cli): extract helpers to reduce duplication
alex-alecu 97bad6d
fix(cli): exempt plan files and protect global config dir from silent…
alex-alecu d92ea98
Merge branch 'main' into feat/config-permission-protection
alex-alecu e4600cd
fix(cli): normalize paths to prevent .. bypass
alex-alecu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
129 changes: 129 additions & 0 deletions
129
packages/opencode/src/kilocode/permission/config-paths.ts
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| import path from "path" | ||
| import { Global } from "@/global" | ||
| import { KilocodePaths } from "@/kilocode/paths" | ||
|
|
||
| export namespace ConfigProtection { | ||
| /** | ||
| * Config directory prefixes (relative paths, forward-slash normalized). | ||
| * Matches .kilo/, .kilocode/, .opencode/ at any depth within the project. | ||
| */ | ||
| const CONFIG_DIRS = [".kilo/", ".kilocode/", ".opencode/"] | ||
|
|
||
| /** | ||
| * Subdirectories under CONFIG_DIRS that are NOT config files (e.g. plan files). | ||
| * Paths under these subdirs are exempt from config protection. | ||
| */ | ||
| const EXCLUDED_SUBDIRS = ["plans/"] | ||
|
|
||
| /** | ||
| * Root-level config files that must be protected. | ||
| * Matched only when the relative path has no directory component. | ||
| */ | ||
| const CONFIG_ROOT_FILES = new Set(["kilo.json", "kilo.jsonc", "opencode.json", "opencode.jsonc", "AGENTS.md"]) | ||
|
|
||
| /** Metadata key used to signal the UI to hide the "Allow always" option. */ | ||
| export const DISABLE_ALWAYS_KEY = "disableAlways" as const | ||
|
|
||
| function normalize(p: string): string { | ||
| return path.posix.normalize(p.replaceAll("\\", "/")) | ||
| } | ||
|
|
||
| /** Return the remainder after the config dir prefix, or undefined if excluded. */ | ||
| function excluded(remainder: string): boolean { | ||
| return EXCLUDED_SUBDIRS.some((sub) => remainder.startsWith(sub)) | ||
| } | ||
|
|
||
| /** Check if a project-relative path points to a config file or directory. */ | ||
| export function isRelative(pattern: string): boolean { | ||
| const normalized = normalize(pattern) | ||
| for (const dir of CONFIG_DIRS) { | ||
| const bare = dir.slice(0, -1) // e.g. ".kilo" | ||
| // Match at root (e.g. ".kilo/foo") or nested (e.g. "packages/sub/.kilo/foo") | ||
| if (normalized === bare || normalized.endsWith("/" + bare)) return true | ||
| if (normalized.startsWith(dir)) { | ||
| if (excluded(normalized.slice(dir.length))) continue | ||
|
alex-alecu marked this conversation as resolved.
|
||
| return true | ||
| } | ||
| const nested = normalized.indexOf("/" + dir) | ||
| if (nested !== -1) { | ||
| if (excluded(normalized.slice(nested + 1 + dir.length))) continue | ||
| return true | ||
| } | ||
| } | ||
| return CONFIG_ROOT_FILES.has(normalized) | ||
| } | ||
|
|
||
| /** Check if `child` is equal to or nested inside `parent`. */ | ||
| function within(child: string, parent: string): boolean { | ||
| return child === parent || child.startsWith(parent + path.sep) | ||
| } | ||
|
|
||
| /** Check if an absolute path is inside a known CLI config directory. */ | ||
| export function isAbsolute(filepath: string): boolean { | ||
| const resolved = path.resolve(filepath) | ||
|
|
||
| // ~/.config/kilo/ (XDG config) | ||
| if (within(resolved, path.resolve(Global.Path.config))) return true | ||
|
|
||
| // ~/.kilo/ and ~/.kilocode/ (legacy global dirs) | ||
| for (const dir of KilocodePaths.globalDirs()) { | ||
| if (within(resolved, path.resolve(dir))) return true | ||
| } | ||
|
|
||
| return false | ||
| } | ||
|
|
||
| /** Check a single path (absolute or relative) against config protection. */ | ||
| function protected_(p: string): boolean { | ||
| return path.isAbsolute(p) ? isAbsolute(p) : isRelative(p) | ||
| } | ||
|
|
||
| /** | ||
| * Determine if a permission request targets config files. | ||
| * Checks `edit` and `external_directory` permissions — read access is not restricted. | ||
| */ | ||
| export function isRequest(request: { | ||
| permission: string | ||
| patterns: string[] | ||
| metadata?: Record<string, any> | ||
| }): boolean { | ||
| // external_directory patterns are absolute globs like "/Users/alex/.config/kilo/*" | ||
| if (request.permission === "external_directory") { | ||
| for (const pattern of request.patterns) { | ||
| const dir = pattern.replace(/\/\*$/, "") | ||
| if (isAbsolute(dir)) return true | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| if (request.permission !== "edit") return false | ||
|
|
||
| // Check patterns — handle both relative and absolute | ||
| for (const pattern of request.patterns) { | ||
| if (protected_(pattern)) return true | ||
| } | ||
|
|
||
| // Check metadata.filepath (absolute for edit, comma-joined relative for apply_patch) | ||
| const fp = request.metadata?.filepath | ||
| if (typeof fp === "string") { | ||
| // apply_patch joins relative paths with ", " | ||
| const parts = fp.includes(", ") ? fp.split(", ") : [fp] | ||
| for (const part of parts) { | ||
| if (protected_(part)) return true | ||
| } | ||
| } | ||
|
|
||
| // Check metadata.files[] (apply_patch file objects with absolute filePath/movePath) | ||
| const files = request.metadata?.files | ||
| if (Array.isArray(files)) { | ||
| for (const file of files) { | ||
| for (const key of ["filePath", "movePath"] as const) { | ||
| const val = file?.[key] | ||
| if (typeof val === "string" && protected_(val)) return true | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return false | ||
| } | ||
| } | ||
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
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
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.