Aleph
Concepts

Discovery

Unified component discovery across multiple directories with upward traversal and on-demand MCP / tool discovery.

The discovery module provides a unified directory-scanning system that finds configuration files, skills, commands, agents, and plugins across multiple roots. It also owns the on-demand discovery contract — the model no longer relies on a pre-built index but reaches for mcp_list_* and tool_search at call time.

Design Philosophy

  1. Multi-source resolution — components can live in multiple locations; later entries override earlier ones
  2. Upward traversal — project-level config files are found by walking from the working directory up to the git root
  3. Claude Code compatibility — reads .claude/ for interop with existing workflows; when both .aleph/ and .claude/ exist at the project level, the native .aleph/ wins on a name clash (so a project's own skill isn't shadowed by a same-named .claude/ entry)
  4. On-demand MCP / tool discovery — no eager indexing of MCP resources or prompts, no full schemas in the initial tool list; the model discovers on demand via mcp_list_* and tool_search

Directory Strategy

Read Paths (by priority, later overrides earlier)

PriorityPathNotes
0~/.claude/Claude Code global (read-only)
10~/.aleph/Aleph global
20+<project>/.claude/Project-level Claude Code (read-only)
40+<project>/.aleph/Project-level Aleph native (outranks the project .claude/ on clash)

Each global / project root's ~/.aleph/{skills,commands,agents,plugins} is scanned. On a same-ID clash between .aleph/ and .claude/, the native .aleph/ entry wins.

Write Paths

  • Writes always go to ~/.aleph/; .claude/ is never written

Core Components

DiscoveryManager

pub struct DiscoveryConfig {
    pub working_dir: PathBuf,
    pub scan_claude_dirs: bool,
    pub scan_project_dirs: bool,
    pub max_upward_depth: usize,    // default 10
}

pub struct DiscoveryManager {
    config: DiscoveryConfig,
    scanner: DirectoryScanner,
}

Key entry points:

MethodDescription
with_defaults()Build with the current directory + default depth
find_config_files(filename)Walk upward for <filename> (e.g. aleph.jsonc); returns [global, …project] — project entries come last and win
discover_skill_dirs() / discover_command_dirs() / discover_agent_dirs()Enumerate child directories under each scan root's <root>/<component>/
discover_plugins()Scan ~/.aleph/plugins/ only (with monorepo one-level descent)
discover_plugins_with_extra(parents)Also scan each project-level <project>/.aleph/plugins/
aleph_home() / working_dir() / git_root()Key path accessors
get_scan_directories()All scan roots in current priority order

DirectoryScanner

The actual filesystem scan lives in src/discovery/scanner.rs:

  • Each child directory must be a real directory (symlink_metadata); symlinks pointing outside the expected tree are rejected
  • Hidden directories (.git, .node_modules, …) are skipped
  • An agents/ subdirectory without an agent.md marker is not a valid extension agent (it could be an Aleph identity directory)
  • Plugin recognition accepts multiple manifest formats: aleph.plugin.toml, aleph.plugin.json, .claude-plugin/plugin.json, .codex-plugin/plugin.json, .cursor-plugin/plugin.json, .cursorrules, .mcp.json, or the presence of any of skills/ / commands/ / agents/ / hooks/ directories, or .cursor/rules/

DiscoveredPath / DiscoverySource

pub struct DiscoveredPath {
    pub path: PathBuf,
    pub source: DiscoverySource,
    pub name: String,
    pub priority: u32,
}

pub enum DiscoverySource {
    AlephGlobal,    // ~/.aleph/
    ClaudeGlobal,   // ~/.claude/
    Project,        // <project>/.claude or .aleph
    Plugin,         // provided by a loaded plugin
}

is_read_only() returns true for ClaudeGlobal and Project.


Upward Traversal

let manager = DiscoveryManager::with_defaults()?;
let configs = manager.find_config_files("aleph.jsonc")?;
// Walks from working_dir up to the git root (or max_upward_depth),
// returning [global, …project].

Edge case: canonicalize() may succeed for the stop directory but fail for the current directory (or vice versa), causing the stop comparison to never match. find_upward tracks whether the stop was canonicalized and compares against both the raw and canonicalized form; max_upward_depth is the hard cap. find_upward also calls validate_path_component, which rejects empty strings, / / \, .. segments, and \0 — even when scan_project_dirs = false so the global branch cannot be exploited via path traversal.


On-Demand MCP / Tool Discovery

MCP: the old McpResourceIndexLayer (eager index layer) has been retracted. The model now discovers MCP resources and prompts through three capability-gated builtin tools:

ToolDescription
mcp_list_resourcesList concrete resource URIs exposed by current MCP servers
mcp_list_resource_templatesList URI templates (covers servers whose resource_count == 0 and only expose templates)
mcp_list_promptsList prompt names available for mcp_get_prompt

Each mcp_list_* ships alongside its reader (mcp_read_resource / mcp_get_prompt) and is only registered in the tool registry while at least one connected server advertises that capability — the model never sees a tool that every call would reject. mcp_tool_bridge is the single channel that translates MCP manager events into registry mutations.

Tools / tool_search: non-core tool schemas are collapsed — the model sees only name + description, and pulls the full schema back on demand via get_tool_schema. tool_search is a self-contained BM25 meta-tool (no new dependencies) covering every deferred tool: the model queries by capability, gets the top matches with full schemas, and calls them directly. [tools] defer_mcp_tools pushes MCP tools into the deferred tier as well. MCP-qualified names ({server}__{tool}) are exempt from the builtin collapse tables.


Plugin Directory Recognition

Each child of ~/.aleph/plugins/ is treated as a plugin root (direct manifest) or a monorepo (one more level down — scan subdirectories for a manifest). The presence of path.join("skills") / commands / agents / hooks is sufficient on its own, and the directory coexists with Claude Code's .claude-plugin/plugin.json.


Plugin Marketplace

  • plugin.marketplace.list / plugin.marketplace.add manage marketplace sources
  • The unified plugin.install entry classifies by source (marketplace / zip / path)
  • Trust tiers: Official / Verified / Community / Unverified
  • Pre-install disclosure: command+args, secrets, network / filesystem reach, pinned version + SHA256, trust tier

See Extensions Store.


Safety Properties

  • No SQL injection — no database queries (filesystem only)
  • No locks — no Mutex / RwLock usage
  • Path-traversal safevalidate_path_component rejects empty strings, / / \, .. segments, \0; symlink_metadata rejects symlinks pointing outside the expected tree; is_hidden() skips .git / .node_modules
  • No UTF-8 slicing traps — all operations stay on char boundaries
  • Git root cannot be hijacked via symlinkfind_git_root delegates to crate::utils::paths::find_git_root; the start path is canonicalized before .git detection, so a .git symlink cannot mis-report an arbitrary ancestor as the repo root

Code Locations

  • src/discovery/mod.rs — module entry + DiscoveryManager
  • src/discovery/scanner.rs — directory scan, upward traversal, plugin manifest recognition
  • src/discovery/paths.rs — path constants + find_upward + validate_path_component
  • src/discovery/types.rsDiscoveredPath / DiscoverySource / ScanDirectory
  • src/mcp/tool_bridge.rs — MCP ↔ tool registry bridge + capability gates
  • src/builtin_tools/mcp_resource.rsmcp_list_resources / mcp_list_resource_templates / mcp_read_resource
  • src/builtin_tools/mcp_prompt.rsmcp_list_prompts / mcp_get_prompt
  • src/tools/tool_search.rstool_search BM25 retriever + ToolSearchTool
  • src/tools/scoped/deferred.rs — deferred tier + tool_search promotion

On this page