@@ -14,8 +14,10 @@ import (
1414 "sync"
1515 "time"
1616
17+ "github.com/spf13/afero"
1718 "go.mondoo.com/mql/v13/llx"
1819 "go.mondoo.com/mql/v13/providers-sdk/v1/plugin"
20+ "go.mondoo.com/mql/v13/providers/os/connection/shared"
1921 "go.mondoo.com/mql/v13/types"
2022 "sigs.k8s.io/yaml"
2123)
@@ -34,16 +36,25 @@ func initClaudeCode(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[
3436 }
3537
3638 if _ , ok := args ["configPath" ]; ! ok {
37- home , err := os .UserHomeDir ()
39+ // Resolve the home directory from the target's user list, not the local host.
40+ home , err := targetHomeDir (runtime )
3841 if err != nil {
39- return nil , nil , fmt . Errorf ( "cannot determine user home directory: %w" , err )
42+ return nil , nil , err
4043 }
4144 args ["configPath" ] = llx .StringData (filepath .Join (home , defaultClaudeCodeConfigDir ))
4245 }
4346
4447 return args , nil , nil
4548}
4649
50+ // mqlClaudeCodeInternal caches the backup state for the lifetime of
51+ // this resource instance, avoiding the global map that leaked across assets.
52+ type mqlClaudeCodeInternal struct {
53+ backupOnce sync.Once
54+ backupState * claudeBackupState
55+ backupErr error
56+ }
57+
4758func (r * mqlClaudeCode ) id () (string , error ) {
4859 return "claude.code/" + r .ConfigPath .Data , nil
4960}
@@ -53,6 +64,12 @@ func (r *mqlClaudeCode) configDir() string {
5364 return r .ConfigPath .Data
5465}
5566
67+ // afs returns an afero.Afero wrapping the connection's filesystem.
68+ func (r * mqlClaudeCode ) afs () * afero.Afero {
69+ conn := r .MqlRuntime .Connection .(shared.Connection )
70+ return & afero.Afero {Fs : conn .FileSystem ()}
71+ }
72+
5673func (r * mqlClaudeCode ) email () (string , error ) {
5774 acct , err := r .loadOAuthAccount ()
5875 if err != nil {
@@ -103,7 +120,7 @@ func (r *mqlClaudeCode) organizationId() (string, error) {
103120
104121func (r * mqlClaudeCode ) settings () (interface {}, error ) {
105122 var settings map [string ]interface {}
106- err := claudeReadJSONFile ( r .configDir (), "settings.json" , & settings )
123+ err := readJSONFileAfero ( r . afs (), r .configDir (), "settings.json" , & settings )
107124 if err != nil {
108125 if os .IsNotExist (err ) {
109126 return map [string ]interface {}{}, nil
@@ -117,7 +134,7 @@ func (r *mqlClaudeCode) enabledPlugins() ([]interface{}, error) {
117134 var settings struct {
118135 EnabledPlugins map [string ]bool `json:"enabledPlugins"`
119136 }
120- err := claudeReadJSONFile ( r .configDir (), "settings.json" , & settings )
137+ err := readJSONFileAfero ( r . afs (), r .configDir (), "settings.json" , & settings )
121138 if err != nil {
122139 if os .IsNotExist (err ) {
123140 return nil , nil
@@ -137,11 +154,13 @@ func (r *mqlClaudeCode) enabledPlugins() ([]interface{}, error) {
137154}
138155
139156func (r * mqlClaudeCode ) plugins () ([]interface {}, error ) {
157+ afs := r .afs ()
158+
140159 var installedPlugins struct {
141160 Version int `json:"version"`
142161 Plugins map [string ][]installedPluginEntry `json:"plugins"`
143162 }
144- err := claudeReadJSONFile ( r .configDir (), "plugins/installed_plugins.json" , & installedPlugins )
163+ err := readJSONFileAfero ( afs , r .configDir (), "plugins/installed_plugins.json" , & installedPlugins )
145164 if err != nil {
146165 if os .IsNotExist (err ) {
147166 return nil , nil
@@ -152,7 +171,7 @@ func (r *mqlClaudeCode) plugins() ([]interface{}, error) {
152171 var settings struct {
153172 EnabledPlugins map [string ]bool `json:"enabledPlugins"`
154173 }
155- _ = claudeReadJSONFile ( r .configDir (), "settings.json" , & settings )
174+ _ = readJSONFileAfero ( afs , r .configDir (), "settings.json" , & settings )
156175
157176 var result []interface {}
158177 for name , entries := range installedPlugins .Plugins {
@@ -184,9 +203,10 @@ func (r *mqlClaudeCode) plugins() ([]interface{}, error) {
184203}
185204
186205func (r * mqlClaudeCode ) skills () ([]interface {}, error ) {
206+ afs := r .afs ()
187207 skillsDir := filepath .Join (r .configDir (), "skills" )
188208
189- entries , err := os .ReadDir (skillsDir )
209+ entries , err := afs .ReadDir (skillsDir )
190210 if err != nil {
191211 if os .IsNotExist (err ) {
192212 return nil , nil
@@ -201,7 +221,7 @@ func (r *mqlClaudeCode) skills() ([]interface{}, error) {
201221 }
202222
203223 skillPath := filepath .Join (skillsDir , entry .Name (), "SKILL.md" )
204- data , err := os .ReadFile (skillPath )
224+ data , err := afs .ReadFile (skillPath )
205225 if err != nil {
206226 continue
207227 }
@@ -231,6 +251,7 @@ func (r *mqlClaudeCode) skills() ([]interface{}, error) {
231251}
232252
233253func (r * mqlClaudeCode ) projects () ([]interface {}, error ) {
254+ afs := r .afs ()
234255 state , err := r .loadBackupState ()
235256 if err != nil {
236257 return nil , err
@@ -240,7 +261,7 @@ func (r *mqlClaudeCode) projects() ([]interface{}, error) {
240261 var result []interface {}
241262 for projectPath , dirName := range state .projectDirMap () {
242263 memoryDir := filepath .Join (projectsDir , dirName , "memory" )
243- hasMemory := dirHasFiles ( memoryDir )
264+ hasMemory := dirHasFilesAfero ( afs , memoryDir )
244265
245266 res , err := NewResource (r .MqlRuntime , "claude.code.project" , map [string ]* llx.RawData {
246267 "__id" : llx .StringData ("claude.code.project/" + projectPath ),
@@ -259,7 +280,7 @@ func (r *mqlClaudeCode) mcpServers() ([]interface{}, error) {
259280 var cache map [string ]struct {
260281 Timestamp int64 `json:"timestamp"`
261282 }
262- err := claudeReadJSONFile ( r .configDir (), "mcp-needs-auth-cache.json" , & cache )
283+ err := readJSONFileAfero ( r . afs (), r .configDir (), "mcp-needs-auth-cache.json" , & cache )
263284 if err != nil {
264285 if os .IsNotExist (err ) {
265286 return nil , nil
@@ -274,6 +295,8 @@ func (r *mqlClaudeCode) mcpServers() ([]interface{}, error) {
274295 lastChecked = time .UnixMilli (entry .Timestamp ).UTC ().Format (time .RFC3339 )
275296 }
276297
298+ // Presence in mcp-needs-auth-cache.json means the server requires
299+ // authentication; servers that don't need auth are not listed.
277300 res , err := NewResource (r .MqlRuntime , "claude.code.mcpServer" , map [string ]* llx.RawData {
278301 "__id" : llx .StringData ("claude.code.mcpServer/" + name ),
279302 "name" : llx .StringData (name ),
@@ -341,45 +364,25 @@ func pathToProjectDir(path string) string {
341364 return "-" + s
342365}
343366
344- // backupStateOnce caches the backup state per resource instance,
345- // loaded at most once via sync.Once.
346- type backupStateOnce struct {
347- once sync.Once
348- state * claudeBackupState
349- err error
350- }
351-
352- var (
353- backupStateInstances = make (map [string ]* backupStateOnce )
354- backupStateInstancesMu sync.Mutex
355- )
356-
357367func (r * mqlClaudeCode ) loadBackupState () (* claudeBackupState , error ) {
358- dir := r .configDir ()
359-
360- backupStateInstancesMu .Lock ()
361- bso , ok := backupStateInstances [dir ]
362- if ! ok {
363- bso = & backupStateOnce {}
364- backupStateInstances [dir ] = bso
365- }
366- backupStateInstancesMu .Unlock ()
368+ r .backupOnce .Do (func () {
369+ afs := r .afs ()
370+ dir := r .configDir ()
367371
368- bso .once .Do (func () {
369- backupFile , err := findLatestBackup (dir )
372+ backupFile , err := findLatestBackupAfero (afs , dir )
370373 if err != nil {
371- bso . err = err
374+ r . backupErr = err
372375 return
373376 }
374377 var state claudeBackupState
375- if err := claudeReadJSONFile ( dir , filepath .Join ("backups" , backupFile ), & state ); err != nil {
376- bso . err = err
378+ if err := readJSONFileAfero ( afs , dir , filepath .Join ("backups" , backupFile ), & state ); err != nil {
379+ r . backupErr = err
377380 return
378381 }
379- bso . state = & state
382+ r . backupState = & state
380383 })
381384
382- return bso . state , bso . err
385+ return r . backupState , r . backupErr
383386}
384387
385388func (r * mqlClaudeCode ) loadOAuthAccount () (* oauthAccount , error ) {
@@ -393,9 +396,9 @@ func (r *mqlClaudeCode) loadOAuthAccount() (*oauthAccount, error) {
393396 return state .OAuthAccount , nil
394397}
395398
396- func findLatestBackup ( configDir string ) (string , error ) {
399+ func findLatestBackupAfero ( afs * afero. Afero , configDir string ) (string , error ) {
397400 backupsDir := filepath .Join (configDir , "backups" )
398- entries , err := os .ReadDir (backupsDir )
401+ entries , err := afs .ReadDir (backupsDir )
399402 if err != nil {
400403 return "" , fmt .Errorf ("cannot read backups directory: %w" , err )
401404 }
@@ -424,9 +427,10 @@ func findLatestBackup(configDir string) (string, error) {
424427 return latestBackup , nil
425428}
426429
427- // claudeReadJSONFile reads and unmarshals a JSON file relative to a base directory.
428- func claudeReadJSONFile (baseDir string , relPath string , v interface {}) error {
429- data , err := os .ReadFile (filepath .Join (baseDir , relPath ))
430+ // readJSONFileAfero reads and unmarshals a JSON file relative to a base directory
431+ // using the provided afero filesystem (which may be remote via SSH, container, etc.).
432+ func readJSONFileAfero (afs * afero.Afero , baseDir string , relPath string , v interface {}) error {
433+ data , err := afs .ReadFile (filepath .Join (baseDir , relPath ))
430434 if err != nil {
431435 return err
432436 }
@@ -482,8 +486,8 @@ func parseSkillMd(name, sourcePath, content string) skillInfo {
482486 return info
483487}
484488
485- func dirHasFiles ( dir string ) bool {
486- entries , err := os .ReadDir (dir )
489+ func dirHasFilesAfero ( afs * afero. Afero , dir string ) bool {
490+ entries , err := afs .ReadDir (dir )
487491 if err != nil {
488492 return false
489493 }
@@ -495,6 +499,30 @@ func dirHasFiles(dir string) bool {
495499 return false
496500}
497501
502+ // targetHomeDir resolves the first non-system user's home directory on the target
503+ // via the users resource, so it works for remote SSH/container connections.
504+ func targetHomeDir (runtime * plugin.Runtime ) (string , error ) {
505+ usersResource , err := CreateResource (runtime , "users" , map [string ]* llx.RawData {})
506+ if err != nil {
507+ return "" , fmt .Errorf ("cannot list users on target: %w" , err )
508+ }
509+
510+ userList := usersResource .(* mqlUsers ).GetList ()
511+ if userList .Error != nil {
512+ return "" , fmt .Errorf ("cannot list users on target: %w" , userList .Error )
513+ }
514+
515+ for _ , u := range userList .Data {
516+ user := u .(* mqlUser )
517+ home := user .GetHome ().Data
518+ if home != "" && ! invalidHomeDirs [home ] {
519+ return home , nil
520+ }
521+ }
522+
523+ return "" , fmt .Errorf ("no valid user home directory found on target" )
524+ }
525+
498526// Stub ID methods for child resources (they use __id set during creation)
499527
500528func (r * mqlClaudeCodePlugin ) id () (string , error ) {
0 commit comments