Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions SCAN_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ On Windows, `~` refers to the user's home directory (`%USERPROFILE%`). Claude De
| Open Interpreter | `~/.config/open-interpreter/config.yaml` | _(same)_ | OpenSource|
| Codex | `~/.codex/config.toml` | _(same)_ | OpenAI |

## AI Agent Skills

Dev Machine Guard inventories every installed **agent skill** — a directory containing a `SKILL.md` manifest — across Claude Code, Codex, OpenCode, Cursor, the cross-agent `~/.agents` convention, and skills installed via [skills.sh](https://skills.sh). It probes each agent's global, system, project, and plugin skill directories; skills.sh lock files add upstream provenance (joined by symlink-resolved path). Detection is pure filesystem reads (no subprocesses), bounded by a 60-second budget and per-root caps.

**Privacy: only metadata and a single SHA-256 hash of each `SKILL.md` are collected — no other file is ever read, and file contents are never transmitted.** The file census (counts, sizes, timestamps) comes entirely from directory listings and `stat`. For skills installed from a local path, the on-disk source path is never serialized — only the skill's alias.

Per skill, the scan records identity and frontmatter (name, description, version, license, allowed tools), capability flags (load-time shell execution, hooks, plugin manifest, subagent context), a stat-only file census, the `SKILL.md` hash, and — when lock-managed — upstream provenance.

## IDE Extensions & Plugins

### VS Code-Family Extensions
Expand Down
42 changes: 42 additions & 0 deletions internal/detector/claudeprojects.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package detector

import (
"encoding/json"

"github.com/step-security/dev-machine-guard/internal/executor"
)

// discoverClaudeProjects reads the "projects" map from ~/.claude.json and
// returns its keys — the absolute root paths of every project the user has
// opened in Claude Code. This is Claude Code's own project registry and the
// highest-signal source of project roots for per-project scanning (MCP configs,
// agent skills, …).
//
// It never fails a scan: any read error, parse error, or empty/missing map
// yields a nil slice. Paths are returned verbatim and unsorted — callers that
// need determinism must sort/dedupe (both the MCP and skills detectors do).
//
// homeDir is resolved via getHomeDir(exec) internally so callers need not thread
// it through; the result is identical to expanding "~/.claude.json" against the
// scanning user's home.
func discoverClaudeProjects(exec executor.Executor) []string {
claudeJSONPath := expandTilde("~/.claude.json", getHomeDir(exec))

content, err := exec.ReadFile(claudeJSONPath)
if err != nil || len(content) == 0 {
return nil
}

var parsed struct {
Projects map[string]json.RawMessage `json:"projects"`
}
if err := json.Unmarshal(content, &parsed); err != nil || len(parsed.Projects) == 0 {
return nil
}

paths := make([]string, 0, len(parsed.Projects))
for projectPath := range parsed.Projects {
paths = append(paths, projectPath)
}
return paths
}
27 changes: 7 additions & 20 deletions internal/detector/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (d *MCPDetector) Detect(_ context.Context, userIdentity string, enterprise
}

// Discover project-level .mcp.json files from known project paths
for _, projectMCP := range d.discoverProjectMCPConfigs(homeDir) {
for _, projectMCP := range d.discoverProjectMCPConfigs() {
results = append(results, model.MCPConfig{
ConfigSource: projectMCP.SourceName,
ConfigPath: projectMCP.ConfigPath,
Expand Down Expand Up @@ -106,7 +106,7 @@ func (d *MCPDetector) DetectEnterprise(_ context.Context) []model.MCPConfigEnter
}

// Discover project-level .mcp.json files from known project paths
for _, projectMCP := range d.discoverProjectMCPConfigs(homeDir) {
for _, projectMCP := range d.discoverProjectMCPConfigs() {
content, err := d.exec.ReadFile(projectMCP.ConfigPath)
if err != nil || len(content) == 0 {
continue
Expand All @@ -128,27 +128,14 @@ func (d *MCPDetector) DetectEnterprise(_ context.Context) []model.MCPConfigEnter
return results
}

// discoverProjectMCPConfigs finds project-level .mcp.json files by reading project paths
// from ~/.claude.json's "projects" section.
func (d *MCPDetector) discoverProjectMCPConfigs(homeDir string) []mcpConfigSpec {
claudeJSONPath := expandTilde("~/.claude.json", homeDir)

content, err := d.exec.ReadFile(claudeJSONPath)
if err != nil || len(content) == 0 {
return nil
}

var parsed struct {
Projects map[string]json.RawMessage `json:"projects"`
}
if err := json.Unmarshal(content, &parsed); err != nil || len(parsed.Projects) == 0 {
return nil
}

// discoverProjectMCPConfigs finds project-level .mcp.json files in the roots
// from Claude Code's project registry (~/.claude.json). Project-root discovery
// is shared with the skills detector via discoverClaudeProjects.
func (d *MCPDetector) discoverProjectMCPConfigs() []mcpConfigSpec {
var specs []mcpConfigSpec
seen := make(map[string]bool)

for projectPath := range parsed.Projects {
for _, projectPath := range discoverClaudeProjects(d.exec) {
mcpPath := filepath.Join(projectPath, ".mcp.json")
if seen[mcpPath] {
continue
Expand Down
Loading
Loading