(
redirections: Array<{ target: string; operator: '>' | '>>' }>,
cwd: string,
toolPermissionContext: ToolPermissionContext,
compoundCommandHasCd?: boolean,
)
| 922 | } |
| 923 | |
| 924 | function validateOutputRedirections( |
| 925 | redirections: Array<{ target: string; operator: '>' | '>>' }>, |
| 926 | cwd: string, |
| 927 | toolPermissionContext: ToolPermissionContext, |
| 928 | compoundCommandHasCd?: boolean, |
| 929 | ): PermissionResult { |
| 930 | // SECURITY: Block output redirections in compound commands containing 'cd' |
| 931 | // This prevents bypassing path safety checks via directory changes before redirections. |
| 932 | // Example attack: cd .claude/ && echo "malicious" > settings.json |
| 933 | // The redirection target would be validated relative to the original CWD, but the |
| 934 | // actual write happens in the changed directory after 'cd' executes. |
| 935 | if (compoundCommandHasCd && redirections.length > 0) { |
| 936 | return { |
| 937 | behavior: 'ask', |
| 938 | message: `Commands that change directories and write via output redirection require explicit approval to ensure paths are evaluated correctly. For security, Claude Code cannot automatically determine the final working directory when 'cd' is used in compound commands.`, |
| 939 | decisionReason: { |
| 940 | type: 'other', |
| 941 | reason: |
| 942 | 'Compound command contains cd with output redirection - manual approval required to prevent path resolution bypass', |
| 943 | }, |
| 944 | } |
| 945 | } |
| 946 | for (const { target } of redirections) { |
| 947 | // /dev/null is always safe - it discards output |
| 948 | if (target === '/dev/null') { |
| 949 | continue |
| 950 | } |
| 951 | const { allowed, resolvedPath, decisionReason } = validatePath( |
| 952 | target, |
| 953 | cwd, |
| 954 | toolPermissionContext, |
| 955 | 'create', // Treat > and >> as create operations |
| 956 | ) |
| 957 | |
| 958 | if (!allowed) { |
| 959 | const workingDirs = Array.from( |
| 960 | allWorkingDirectories(toolPermissionContext), |
| 961 | ) |
| 962 | const dirListStr = formatDirectoryList(workingDirs) |
| 963 | |
| 964 | // Use security check's custom reason if available (type: 'other' or 'safetyCheck') |
| 965 | // Otherwise use the standard message for deny rules or working directory restrictions |
| 966 | const message = |
| 967 | decisionReason?.type === 'other' || |
| 968 | decisionReason?.type === 'safetyCheck' |
| 969 | ? decisionReason.reason |
| 970 | : decisionReason?.type === 'rule' |
| 971 | ? `Output redirection to '${resolvedPath}' was blocked by a deny rule.` |
| 972 | : `Output redirection to '${resolvedPath}' was blocked. For security, Claude Code may only write to files in the allowed working directories for this session: ${dirListStr}.` |
| 973 | |
| 974 | // If denied by a deny rule, return 'deny' behavior |
| 975 | if (decisionReason?.type === 'rule') { |
| 976 | return { |
| 977 | behavior: 'deny', |
| 978 | message, |
| 979 | decisionReason, |
| 980 | } |
| 981 | } |
no test coverage detected