Skip to content

Commit acded5a

Browse files
committed
Fix claude config file access blocked by path security and add hooks section
1 parent 11c4375 commit acded5a

File tree

7 files changed

+71
-13
lines changed

7 files changed

+71
-13
lines changed

src/main/ipc/claude-config.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,27 @@ function scanClaudeFiles(projectPath: string): ClaudeFileEntry[] {
7676

7777
addFiles(path.join(claudeDir, 'commands'), 'commands', entries)
7878

79+
addFiles(path.join(claudeDir, 'scripts'), 'hooks', entries)
80+
81+
const keybindingsJson = path.join(claudeDir, 'keybindings.json')
82+
if (fs.existsSync(keybindingsJson)) {
83+
entries.push({ name: 'keybindings.json', path: keybindingsJson, section: 'global' })
84+
}
85+
7986
const projectKey = projectPath.replace(/\//g, '-')
80-
const projectMemDir = path.join(claudeDir, 'projects', projectKey, 'memory')
87+
const projectConfigDir = path.join(claudeDir, 'projects', projectKey)
88+
89+
const projectSettingsJson = path.join(projectConfigDir, 'settings.json')
90+
if (fs.existsSync(projectSettingsJson)) {
91+
entries.push({ name: 'settings.json (project)', path: projectSettingsJson, section: 'project' })
92+
}
93+
94+
const projectClaudeMdGlobal = path.join(projectConfigDir, 'CLAUDE.md')
95+
if (fs.existsSync(projectClaudeMdGlobal)) {
96+
entries.push({ name: 'CLAUDE.md (project config)', path: projectClaudeMdGlobal, section: 'project' })
97+
}
98+
99+
const projectMemDir = path.join(projectConfigDir, 'memory')
81100
addFiles(projectMemDir, 'project', entries)
82101

83102
const projectClaudeDir = path.join(projectPath, '.claude')
@@ -103,8 +122,38 @@ function scanClaudeFiles(projectPath: string): ClaudeFileEntry[] {
103122
return entries
104123
}
105124

125+
function isAllowedClaudePath(filePath: string, projectPath?: string): boolean {
126+
const resolved = path.resolve(filePath)
127+
const claudeDir = path.join(os.homedir(), '.claude')
128+
if (resolved.startsWith(claudeDir + path.sep)) return true
129+
if (projectPath) {
130+
const projectClaudeDir = path.join(path.resolve(projectPath), '.claude')
131+
if (resolved.startsWith(projectClaudeDir + path.sep)) return true
132+
if (resolved === path.join(path.resolve(projectPath), 'CLAUDE.md')) return true
133+
}
134+
return false
135+
}
136+
106137
export function registerClaudeConfigHandlers(): void {
107138
ipcMain.handle('claude:scan-files', (_event, projectPath: string): ClaudeFileEntry[] => {
108139
return scanClaudeFiles(projectPath)
109140
})
141+
142+
ipcMain.handle('claude:read-file', (_event, filePath: string, projectPath: string): string => {
143+
const resolved = path.resolve(filePath)
144+
if (!isAllowedClaudePath(resolved, projectPath)) throw new Error('Path not allowed')
145+
return fs.readFileSync(resolved, 'utf-8')
146+
})
147+
148+
ipcMain.handle('claude:write-file', (_event, filePath: string, content: string, projectPath: string): void => {
149+
const resolved = path.resolve(filePath)
150+
if (!isAllowedClaudePath(resolved, projectPath)) throw new Error('Path not allowed')
151+
fs.writeFileSync(resolved, content, 'utf-8')
152+
})
153+
154+
ipcMain.handle('claude:delete-file', (_event, filePath: string, projectPath: string): void => {
155+
const resolved = path.resolve(filePath)
156+
if (!isAllowedClaudePath(resolved, projectPath)) throw new Error('Path not allowed')
157+
fs.unlinkSync(resolved)
158+
})
110159
}

src/main/models/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export interface EncryptedCredential {
8585
updatedAt: number
8686
}
8787

88-
export type ClaudeSection = 'global' | 'skills' | 'commands' | 'project'
88+
export type ClaudeSection = 'global' | 'hooks' | 'skills' | 'commands' | 'project'
8989

9090
export interface ClaudeFileEntry {
9191
name: string

src/preload/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ const api = {
1010
},
1111

1212
claude: {
13-
scanFiles: (projectPath: string) => ipcRenderer.invoke('claude:scan-files', projectPath)
13+
scanFiles: (projectPath: string) => ipcRenderer.invoke('claude:scan-files', projectPath),
14+
readFile: (filePath: string, projectPath: string) => ipcRenderer.invoke('claude:read-file', filePath, projectPath),
15+
writeFile: (filePath: string, content: string, projectPath: string) => ipcRenderer.invoke('claude:write-file', filePath, content, projectPath),
16+
deleteFile: (filePath: string, projectPath: string) => ipcRenderer.invoke('claude:delete-file', filePath, projectPath)
1417
},
1518

1619
fs: {

src/renderer/components/claude/ClaudeEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export function ClaudeEditor({ projectId }: { projectId: string }): React.ReactE
5050
keybindings: [2048 | 49],
5151
run: async () => {
5252
if (activeFilePath) {
53-
await saveFile(activeFilePath, editorInstance.getValue())
53+
await saveFile(projectId, activeFilePath, editorInstance.getValue())
5454
flashSaved()
5555
}
5656
}

src/renderer/components/claude/ClaudeFileList.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { useEffect } from 'react'
22
import { useClaudeStore } from '@/stores/claude-store'
33
import { useProjectStore } from '@/stores/project-store'
4-
import { ChevronRight, ChevronDown, File, Globe, Wand2, Terminal, FolderOpen, RefreshCw, Trash2 } from 'lucide-react'
4+
import { ChevronRight, ChevronDown, File, Globe, Wand2, Terminal, FolderOpen, Webhook, RefreshCw, Trash2 } from 'lucide-react'
55
import type { ClaudeSection, ClaudeFileEntry } from '@/models/types'
66

77
const SECTION_CONFIG: { key: ClaudeSection; label: string; Icon: typeof Globe }[] = [
88
{ key: 'global', label: 'Global', Icon: Globe },
9+
{ key: 'hooks', label: 'Hooks', Icon: Webhook },
910
{ key: 'skills', label: 'Skills', Icon: Wand2 },
1011
{ key: 'commands', label: 'Commands', Icon: Terminal },
1112
{ key: 'project', label: 'Project', Icon: FolderOpen }

src/renderer/models/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export interface PasswordPromptData {
118118
password: string
119119
}
120120

121-
export type ClaudeSection = 'global' | 'skills' | 'commands' | 'project'
121+
export type ClaudeSection = 'global' | 'hooks' | 'skills' | 'commands' | 'project'
122122

123123
export interface ClaudeFileEntry {
124124
name: string

src/renderer/stores/claude-store.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,28 @@ interface ClaudeStore {
66
activeFilePerProject: Record<string, string | null>
77
contentCache: Record<string, string>
88
expandedSections: Record<string, Set<ClaudeSection>>
9+
projectPaths: Record<string, string>
910
loadFiles: (projectId: string, projectPath: string) => Promise<void>
1011
selectFile: (projectId: string, filePath: string) => Promise<void>
11-
saveFile: (filePath: string, content: string) => Promise<void>
12+
saveFile: (projectId: string, filePath: string, content: string) => Promise<void>
1213
deleteFile: (projectId: string, filePath: string, projectPath: string) => Promise<void>
1314
toggleSection: (projectId: string, section: ClaudeSection) => void
1415
}
1516

16-
const DEFAULT_EXPANDED = new Set<ClaudeSection>(['global', 'skills', 'commands', 'project'])
17+
const DEFAULT_EXPANDED = new Set<ClaudeSection>(['global', 'hooks', 'skills', 'commands', 'project'])
1718

1819
export const useClaudeStore = create<ClaudeStore>((set, get) => ({
1920
filesPerProject: {},
2021
activeFilePerProject: {},
2122
contentCache: {},
2223
expandedSections: {},
24+
projectPaths: {},
2325

2426
loadFiles: async (projectId: string, projectPath: string) => {
2527
const files: ClaudeFileEntry[] = await window.api.claude.scanFiles(projectPath)
2628
set((s) => ({
27-
filesPerProject: { ...s.filesPerProject, [projectId]: files }
29+
filesPerProject: { ...s.filesPerProject, [projectId]: files },
30+
projectPaths: { ...s.projectPaths, [projectId]: projectPath }
2831
}))
2932
},
3033

@@ -36,22 +39,24 @@ export const useClaudeStore = create<ClaudeStore>((set, get) => ({
3639
}))
3740
return
3841
}
39-
const { content } = await window.api.fs.readFile(filePath)
42+
const projectPath = get().projectPaths[projectId] ?? ''
43+
const content: string = await window.api.claude.readFile(filePath, projectPath)
4044
set((s) => ({
4145
activeFilePerProject: { ...s.activeFilePerProject, [projectId]: filePath },
4246
contentCache: { ...s.contentCache, [filePath]: content }
4347
}))
4448
},
4549

46-
saveFile: async (filePath: string, content: string) => {
47-
await window.api.fs.writeFile(filePath, content)
50+
saveFile: async (projectId: string, filePath: string, content: string) => {
51+
const projectPath = get().projectPaths[projectId] ?? ''
52+
await window.api.claude.writeFile(filePath, content, projectPath)
4853
set((s) => ({
4954
contentCache: { ...s.contentCache, [filePath]: content }
5055
}))
5156
},
5257

5358
deleteFile: async (projectId: string, filePath: string, projectPath: string) => {
54-
await window.api.fs.deleteFile(filePath)
59+
await window.api.claude.deleteFile(filePath, projectPath)
5560
const { contentCache, activeFilePerProject } = get()
5661
const nextCache = { ...contentCache }
5762
delete nextCache[filePath]

0 commit comments

Comments
 (0)