Skip to content
Merged
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
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

A TUI for inspecting agent CLI configurations: what's installed, what's enabled, what's broken.

agentpeek scans your Claude Code config (`~/.claude/` and any project-level `.claude/` directory it finds by walking up from `cwd`) and surfaces every loose file the agent depends on: settings, hooks, slash commands, memory files, MCP servers, installed plugins (with their full contents: manifest, skills, agents, commands, hooks, MCPs), and any health warnings detected during the scan.
agentpeek scans your Claude Code config (`~/.claude/` and any project-level `.claude/` directory it finds by walking up from `cwd`) and surfaces every loose file the agent depends on: settings, hooks, slash commands, memory files, MCP servers, user/project subagents, path-scoped rules, installed plugins (with their full contents: manifest, skills, agents, commands, hooks, MCPs), and any health warnings detected during the scan.

## Install

Expand All @@ -22,12 +22,16 @@ Requires Python ≥ 3.11. Linux and macOS only.

## What you get

- **Three-zone TUI**: sidebar of 9 categories on the left, items list in the middle, detail pane on the right. Drill-down on every category.
- **Three-zone TUI**: sidebar of 11 categories on the left, items list in the middle, detail pane on the right. Drill-down on every category.
- **Multi-scope**: user-scoped (`~/.claude/`) and project-scoped (`.claude/` found by walking up from cwd) configurations shown side by side with `[U]` / `[P]` badges. Health checks compare the two and flag conflicts.
- **Plugin contents**: every installed plugin is opened up - manifest, skills, agents, slash commands, hooks, MCP servers, and the marketplace's auto-update state. Plugin-contributed commands / hooks / MCPs also merge into the top-level categories with a `[plug:<id>]` provenance tag.
- **Auto-memory**: Claude Code project memories (under `~/.claude/projects/<encoded>/memory/`) are scanned and labelled with their actual project path (resolved from session logs, not the lossy directory encoding).
- **Health checks**: orphan hook scripts, missing plugin install paths, conflicting key bindings, scope override conflicts, cross-scope layered memory, hooks referencing dynamic env vars (`${CLAUDE_PROJECT_DIR}`, `$HOME`, …) flagged so you know which won't resolve at scan time, and more. Severity-coloured warning cards in the detail pane.
- **Write actions**: update, enable/disable, uninstall a plugin or refresh a marketplace without leaving the TUI. Calls shell out to `claude plugin …`; agentpeek never edits Claude state files directly. Re-scan is automatic after every action. Pass `--read-only` to disable.
- **Subagents**: user and project agents under `agents/*.md` are scanned recursively, with full frontmatter (tools, model, permission mode, memory scope, color, and the rest) shown in the detail card.
- **Path-scoped rules**: `.claude/rules/*.md` files at user and project scope are listed with their `paths:` globs (or marked "always loaded" when the field is absent), so you can answer "why is Claude doing X here?" by category instead of by guessing.
- **Plugin contents**: every installed plugin is opened up: manifest, skills, agents, slash commands, hooks, MCP servers, and the marketplace's auto-update state. Plugin-contributed commands / hooks / MCPs also merge into the top-level categories with a `[plug:<id>]` provenance tag.
- **Memory layer**: `CLAUDE.md`, `CLAUDE.local.md` (project-root personal overrides), and subagent persistent memory directories (`agent-memory/` and `agent-memory-local/`) are all surfaced. CLAUDE.md `@path/to/file` imports are resolved recursively (up to 5 hops, cycles detected) so you see what Claude actually loads.
- **Auto-memory**: Claude Code project memories under `~/.claude/projects/<encoded>/memory/` are scanned and labelled with their actual project path (resolved from session logs, not the lossy directory encoding).
- **`~/.claude.json`**: user-scope MCP servers, per-project trust state (allowed tools, enabled / disabled `.mcp.json` servers), and OAuth session presence (display-only - the token value is never read or shown). Folded into the MCP and Settings cards.
- **Health checks**: orphan hook scripts, missing plugin install paths, conflicting key bindings, scope override conflicts, cross-scope layered memory, hooks referencing dynamic env vars (`${CLAUDE_PROJECT_DIR}`, `$HOME`, ...) flagged so you know which won't resolve at scan time, and more. Severity-coloured warning cards in the detail pane.
- **Write actions**: update, enable/disable, uninstall a plugin or refresh a marketplace without leaving the TUI. Calls shell out to `claude plugin ...`; agentpeek never edits Claude state files directly. Re-scan is automatic after every action. Pass `--read-only` to disable.

## Keybindings

Expand Down Expand Up @@ -63,4 +67,4 @@ Built on Textual: the theme follows `Ctrl+P` (palette) and reflows colours acros

## License

GPL-3.0-only. See `LICENSE`.
GPL-3.0-only. See [LICENSE](LICENSE).
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "agentpeek"
version = "0.12.1"
version = "0.13.0"
description = "TUI inspector for agent CLI configuration directories like ~/.claude/."
readme = "README.md"
requires-python = ">=3.11"
Expand Down
101 changes: 100 additions & 1 deletion src/agentpeek/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ class SettingsBundle:
# surfaced as a list so users can see actual script names without
# opening the filesystem.
hooks_dir_files: tuple[str, ...]
# `agent` setting: default subagent for sessions in this scope.
# Set via `.claude/settings.json` and overridable on the CLI.
default_agent: str | None = None
# `claudeMdExcludes`: glob patterns excluding specific CLAUDE.md
# files from the load order. Surfaced because they answer
# "why isn't this CLAUDE.md taking effect?" debugging questions.
claude_md_excludes: tuple[str, ...] = ()


@dataclass(frozen=True, slots=True)
Expand Down Expand Up @@ -174,7 +181,26 @@ class Plugin:
)


MemoryKind = Literal["claude_md", "memory_index", "memory_entry"]
MemoryKind = Literal[
"claude_md",
"claude_local_md",
"memory_index",
"memory_entry",
"agent_memory_index",
"agent_memory_entry",
]


@dataclass(frozen=True, slots=True)
class MemoryImport:
# Resolved import target encountered while expanding `@path/to/file`
# references inside a CLAUDE.md body. `resolved_path` is None when
# the import couldn't be resolved (file missing, cycle, depth cap
# hit); `reason` carries a short marker for the renderer.
raw: str
resolved_path: Path | None
depth: int
reason: str | None = None


@dataclass(frozen=True, slots=True)
Expand All @@ -184,6 +210,59 @@ class MemoryFile:
has_frontmatter: bool
kind: MemoryKind
project_label: str | None
# Auto-memory entries belonging to a specific subagent carry the
# agent name resolved from the parent directory; otherwise None.
agent_name: str | None = None
# Resolved @path imports referenced from this body (recursive, max 5
# hops, cycles marked). Empty when the file isn't a CLAUDE.md or
# contains no imports.
imports: tuple[MemoryImport, ...] = ()


@dataclass(frozen=True, slots=True)
class Agent:
# User- or project-scope subagent (`~/.claude/agents/` or
# `<project>/.claude/agents/`). Plugin-bundled agents use the
# separate PluginAgent type — they live inside Plugin.agents and
# are constructed by parsers.plugin_contents.
path: Path
name: str
description: str | None
body: str
# `tools` and `disallowedTools` accept either a comma-separated
# string or a YAML list in the canonical schema. Normalized to a
# tuple of token strings here.
tools: tuple[str, ...] = ()
disallowed_tools: tuple[str, ...] = ()
model: str | None = None
permission_mode: str | None = None
max_turns: int | None = None
skills: tuple[str, ...] = ()
memory: str | None = None
background: bool | None = None
effort: str | None = None
isolation: str | None = None
color: str | None = None
initial_prompt: str | None = None
# Raw mcpServers + hooks bodies — both have complex schemas that
# don't compress into a tuple of strings cleanly. We carry them as
# presence flags + a stringified summary so the detail card can
# show "configured" / "(none)".
has_mcp_servers: bool = False
has_hooks: bool = False


@dataclass(frozen=True, slots=True)
class Rule:
# `.claude/rules/*.md` — path-scoped or always-loaded context rule.
# Per docs, `paths:` frontmatter is optional; absent means the rule
# loads at session start with the same priority as `.claude/CLAUDE.md`.
path: Path
name: str
description: str | None
paths_globs: tuple[str, ...]
always_loaded: bool
body: str


@dataclass(frozen=True, slots=True)
Expand Down Expand Up @@ -214,6 +293,19 @@ class MCPServer:
auth_pending: bool = False


@dataclass(frozen=True, slots=True)
class TrustEntry:
# Per-project trust state from `~/.claude.json[projects][<path>]`.
# Surfaced in the Settings detail card so users can see which
# projects they've trusted, which tools are pre-approved, and the
# per-project enable/disable lists for `.mcp.json` servers.
project_path: str
trust_accepted: bool
allowed_tools: tuple[str, ...]
enabled_mcpjson_servers: tuple[str, ...]
disabled_mcpjson_servers: tuple[str, ...]


@dataclass(frozen=True, slots=True)
class ScanResult:
source: str
Expand All @@ -226,6 +318,13 @@ class ScanResult:
keybindings: KeybindingsBundle | None
mcp: tuple[MCPServer, ...]
warnings: tuple[ScanWarning, ...]
agents: tuple[Agent, ...] = ()
rules: tuple[Rule, ...] = ()
# `~/.claude.json` highlights, only populated at user scope. Empty
# tuple when the file is absent or the scope isn't user.
trust_entries: tuple[TrustEntry, ...] = ()
oauth_session_present: bool = False
claude_json_path: Path | None = None

@classmethod
def empty(
Expand Down
Loading
Loading