( tool: Tool<Input>, input: z.infer<Input>, toolPermissionContext: ToolPermissionContext, precomputedPathsToCheck?: readonly string[], )
| 1203 | * stale value would silently check deny rules for the wrong path. |
| 1204 | */ |
| 1205 | export function checkWritePermissionForTool<Input extends AnyObject>( |
| 1206 | tool: Tool<Input>, |
| 1207 | input: z.infer<Input>, |
| 1208 | toolPermissionContext: ToolPermissionContext, |
| 1209 | precomputedPathsToCheck?: readonly string[], |
| 1210 | ): PermissionDecision { |
| 1211 | if (typeof tool.getPath !== 'function') { |
| 1212 | return { |
| 1213 | behavior: 'ask', |
| 1214 | message: `Claude requested permissions to use ${tool.name}, but you haven't granted it yet.`, |
| 1215 | } |
| 1216 | } |
| 1217 | const path = tool.getPath(input) |
| 1218 | |
| 1219 | // 1. Check for deny rules - check both the original path and resolved symlink path |
| 1220 | const pathsToCheck = |
| 1221 | precomputedPathsToCheck ?? getPathsForPermissionCheck(path) |
| 1222 | for (const pathToCheck of pathsToCheck) { |
| 1223 | const denyRule = matchingRuleForInput( |
| 1224 | pathToCheck, |
| 1225 | toolPermissionContext, |
| 1226 | 'edit', |
| 1227 | 'deny', |
| 1228 | ) |
| 1229 | if (denyRule) { |
| 1230 | return { |
| 1231 | behavior: 'deny', |
| 1232 | message: `Permission to edit ${path} has been denied.`, |
| 1233 | decisionReason: { |
| 1234 | type: 'rule', |
| 1235 | rule: denyRule, |
| 1236 | }, |
| 1237 | } |
| 1238 | } |
| 1239 | } |
| 1240 | |
| 1241 | // 1.5. Allow writes to internal editable paths (plan files, scratchpad) |
| 1242 | // This MUST come before isDangerousFilePathToAutoEdit check since .claude is a dangerous directory |
| 1243 | const absolutePathForEdit = expandPath(path) |
| 1244 | const internalEditResult = checkEditableInternalPath( |
| 1245 | absolutePathForEdit, |
| 1246 | input, |
| 1247 | ) |
| 1248 | if (internalEditResult.behavior !== 'passthrough') { |
| 1249 | return internalEditResult |
| 1250 | } |
| 1251 | |
| 1252 | // 1.6. Check for .claude/** allow rules BEFORE safety checks |
| 1253 | // This allows session-level permissions to bypass the safety blocks for .claude/ |
| 1254 | // We only allow this for session-level rules to prevent users from accidentally |
| 1255 | // permanently granting broad access to their .claude/ folder. |
| 1256 | // |
| 1257 | // matchingRuleForInput returns the first match across all sources. If the user |
| 1258 | // also has a broader Edit(.claude) rule in userSettings (e.g. from sandbox |
| 1259 | // write-allow conversion), that rule would be found first and its source check |
| 1260 | // below would fail. Scope the search to session-only rules so the dialog's |
| 1261 | // "allow Claude to edit its own settings for this session" option actually works. |
| 1262 | const claudeFolderAllowRule = matchingRuleForInput( |
no test coverage detected