@@ -9,6 +9,49 @@ import { fileExistsAtPath } from "../../utils/fs"
99import { GlobalFileNames } from "../../shared/globalFileNames"
1010import { getTaskDirectoryPath } from "../../utils/storage"
1111
12+ /**
13+ * Redaction utilities:
14+ * We only need to ensure sensitive file payloads are NOT persisted to disk (ui_messages.json).
15+ * Centralizing the sanitization in the persistence layer keeps Task.ts simple and avoids scattering
16+ * redaction logic across multiple call-sites.
17+ */
18+
19+ function sanitizeMessageText ( text ?: string ) : string | undefined {
20+ if ( ! text ) return text
21+
22+ // Scrub helper that replaces inner contents of known file payload tags with an omission marker
23+ const scrub = ( s : string ) : string => {
24+ // Order matters: scrub more specific tags first
25+ s = s . replace ( / < f i l e _ c o n t e n t \b [ \s \S ] * ?< \/ f i l e _ c o n t e n t > / gi, "<file_content>[omitted]</file_content>" )
26+ s = s . replace ( / < c o n t e n t \b [ ^ > ] * > [ \s \S ] * ?< \/ c o n t e n t > / gi, "<content>[omitted]</content>" )
27+ s = s . replace ( / < f i l e \b [ ^ > ] * > [ \s \S ] * ?< \/ f i l e > / gi, "<file>[omitted]</file>" )
28+ s = s . replace ( / < f i l e s \b [ ^ > ] * > [ \s \S ] * ?< \/ f i l e s > / gi, "<files>[omitted]</files>" )
29+ return s
30+ }
31+
32+ // If JSON payload (e.g. api_req_started), try to sanitize its 'request' field
33+ try {
34+ const obj = JSON . parse ( text )
35+ if ( obj && typeof obj === "object" && typeof obj . request === "string" ) {
36+ obj . request = scrub ( obj . request )
37+ return JSON . stringify ( obj )
38+ }
39+ } catch {
40+ // Not JSON; fall through to raw scrub
41+ }
42+
43+ return scrub ( text )
44+ }
45+
46+ function sanitizeMessages ( messages : ClineMessage [ ] ) : ClineMessage [ ] {
47+ return messages . map ( ( m ) => {
48+ if ( typeof ( m as any ) . text === "string" ) {
49+ return { ...m , text : sanitizeMessageText ( ( m as any ) . text ) }
50+ }
51+ return m
52+ } )
53+ }
54+
1255export type ReadTaskMessagesOptions = {
1356 taskId : string
1457 globalStoragePath : string
@@ -23,7 +66,9 @@ export async function readTaskMessages({
2366 const fileExists = await fileExistsAtPath ( filePath )
2467
2568 if ( fileExists ) {
26- return JSON . parse ( await fs . readFile ( filePath , "utf8" ) )
69+ // Sanitize on read as a safety net for any legacy persisted content
70+ const raw = JSON . parse ( await fs . readFile ( filePath , "utf8" ) )
71+ return sanitizeMessages ( raw )
2772 }
2873
2974 return [ ]
@@ -38,5 +83,8 @@ export type SaveTaskMessagesOptions = {
3883export async function saveTaskMessages ( { messages, taskId, globalStoragePath } : SaveTaskMessagesOptions ) {
3984 const taskDir = await getTaskDirectoryPath ( globalStoragePath , taskId )
4085 const filePath = path . join ( taskDir , GlobalFileNames . uiMessages )
41- await safeWriteJson ( filePath , messages )
86+
87+ // Persist a sanitized copy to disk to avoid storing sensitive file payloads
88+ const sanitized = sanitizeMessages ( messages )
89+ await safeWriteJson ( filePath , sanitized )
4290}
0 commit comments