Skip to content

feat(custom-agents): add filesystem-based loading of Claude subagents from .claude/agents/*.md#1212

Open
mcljot wants to merge 3 commits intoambient-code:mainfrom
mcljot:feature/custom-subagent-loading
Open

feat(custom-agents): add filesystem-based loading of Claude subagents from .claude/agents/*.md#1212
mcljot wants to merge 3 commits intoambient-code:mainfrom
mcljot:feature/custom-subagent-loading

Conversation

@mcljot
Copy link
Copy Markdown

@mcljot mcljot commented Apr 4, 2026

Summary

ACP sessions can only dispatch the 4 built-in agent types (general-purpose, Explore, Plan, statusline-setup). Custom agents defined as .claude/agents/*.md files in a project's repo are ignored because the runner never loads them into the SDK — even though ClaudeAgentOptions.agents supports exactly this via dict[str, AgentDefinition].

This PR adds filesystem-based custom agent loading to the Claude bridge so that .claude/agents/*.md files are parsed at session startup and passed to the SDK as registered subagent types.

Changes

  • New agents.py module — scans .claude/agents/*.md, parses frontmatter (name, description, tools, model, skills) and markdown body
    (prompt), returns dict[str, AgentDefinition]
  • bridge.py — loads custom agents in _setup_platform() after workspace path resolution, passes them via options["agents"] in
    _ensure_adapter()
  • 17 unit tests covering frontmatter parsing, string list conversion, directory loading, fallback defaults, and error handling

Rationale

The .claude/agents/ filesystem loading is a Claude Code CLI feature — the CLI reads those files and converts them to AgentDefinition objects before passing them to the SDK. The ACP runner never did this conversion. The runner already parses these files for the workflow metadata endpoint (content.py), but only to surface names to the frontend UI — it never constructed SDK AgentDefinition objects for actual session dispatch.

The build_options() method in adapter.py already merges the full options dict into ClaudeAgentOptions(**merged_kwargs), so adding "agents" to the dict passes through to the SDK with no adapter changes needed.

Testing

  • cd components/runners/ambient-runner && python -m pytest tests/test_custom_agents.py -v
  • Existing runner tests still pass
  • Deploy to dev cluster with a project containing .claude/agents/*.md files
  • Verify Agent(subagent_type="<custom-agent-name>") dispatches successfully
  • Check runner logs for "Loaded N custom agent(s)" messages

Notes

  • No new dependencies — uses the same manual frontmatter parsing as content.py (no pyyaml)
  • AgentDefinition is from the existing claude-agent-sdk>=0.1.50 dependency
  • Known SDK caveat: anthropics/claude-agent-sdk-python#322 — custom agents are
    silently dropped when combined system prompt + agent definitions exceed an undocumented token limit. This is tracked upstream.

Summary by CodeRabbit

  • New Features

    • Support for workspace-local custom Claude agents via markdown files in a .claude/agents directory. Agent metadata (name, description, model, tools, skills) and prompt body are parsed, with sensible fallbacks when fields are missing.
    • Loader skips invalid files and returns an empty set when no directory exists.
  • Tests

    • Added unit tests covering parsing, list handling, fallbacks, and resilient loading behavior.

mcljot and others added 2 commits April 4, 2026 19:31
The Claude Agent SDK supports custom subagent types via the `agents`
parameter on ClaudeAgentOptions, but the runner never populated it.
This meant sessions could only use the 4 built-in agent types
(general-purpose, Explore, Plan, statusline-setup) — custom agents
defined as .md files in .claude/agents/ were invisible to the SDK.

Add a new agents.py utility that scans .claude/agents/*.md, parses
frontmatter (name, description, tools, model, skills) and the
markdown body (prompt), and constructs AgentDefinition objects. These
are loaded during _setup_platform() and passed through the options
dict in _ensure_adapter(), where build_options() already merges them
into ClaudeAgentOptions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

📝 Walkthrough

Walkthrough

Loads workspace-local Claude agent definitions from .claude/agents markdown files, parses YAML-like frontmatter and body into AgentDefinition objects (name, description, tools, model, skills), and injects the resulting agent map into ClaudeAgentAdapter options during bridge setup.

Changes

Cohort / File(s) Summary
Agent Loading Implementation
components/runners/ambient-runner/ambient_runner/bridges/claude/agents.py
New loader load_agents_from_directory that scans *.md, parses --- frontmatter without external YAML deps, converts comma-separated lists to arrays, applies filename/description fallbacks, logs per-file errors, and returns mapping name→AgentDefinition.
Bridge Integration
components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py
_setup_platform loads workspace .claude/agents via the loader and stores _custom_agents; _ensure_adapter injects options["agents"] when custom agents are present.
Test Coverage
components/runners/ambient-runner/tests/test_custom_agents.py
Adds unit tests for frontmatter parsing, list parsing, missing/quoted/multiline fields, directory scanning behavior, fallback naming/description, and error resilience during AgentDefinition construction.

Sequence Diagram(s)

sequenceDiagram
    participant Bridge as ClaudeBridge
    participant Loader as load_agents_from_directory
    participant FS as File System
    participant Parser as Frontmatter Parser
    participant Factory as AgentDefinition Factory
    participant Adapter as ClaudeAgentAdapter

    Bridge->>+Loader: load_agents_from_directory(agents_dir)
    Loader->>+FS: list *.md in .claude/agents
    FS-->>-Loader: file list
    loop each file
        Loader->>Parser: parse file content (frontmatter → metadata, body)
        Parser-->>Loader: metadata, body
        Loader->>Factory: build AgentDefinition(metadata, body)
        Factory-->>Loader: AgentDefinition
    end
    Loader-->>-Bridge: dict[name → AgentDefinition]
    Bridge->>Adapter: provide options (including agents)
    Adapter-->>Bridge: adapter initialized with custom agents
Loading
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title follows Conventional Commits format with feat type, clear scope (custom-agents), and descriptive summary of the main changeset feature.
Performance And Algorithmic Complexity ✅ Passed Single O(n) filesystem pass, linear file parsing, executed once at session init in non-hot path, result cached. No N+1 patterns or per-request overhead.
Security And Secret Handling ✅ Passed agents.py module contains no security violations: agent prompts never logged, pathlib prevents traversal, no injection vectors, no secrets exposed, safe file parsing.
Kubernetes Resource Safety ✅ Passed Kubernetes Resource Safety check not applicable; PR modifies only Python source files for custom Claude agent loading with no Kubernetes manifests or infrastructure configurations present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/runners/ambient-runner/ambient_runner/bridges/claude/agents.py`:
- Around line 27-30: The current except OSError block that returns {}, "" allows
unreadable files to proceed and later creates an AgentDefinition with an empty
prompt; change the error handling so unreadable files are skipped: in the
file-reading code (the try around file_path.read_text and its except OSError)
return a sentinel (e.g., None) or raise so the caller can filter them out, and
update the agent-building logic that constructs AgentDefinition with the
fallback name/prompt (the AgentDefinition construction path) to ignore any
None/missing inputs instead of building an empty agent.
- Around line 32-40: The frontmatter parsing only looks for Unix newlines (using
content.startswith("---\n") and content.find("\n---", 4)), so files with CRLF
("\r\n") line endings won't match; normalize line endings or make the boundary
detection CRLF-aware before extracting frontmatter. Modify the code that checks
content.startswith, content.find, and computes frontmatter_raw/body (symbols:
content.startswith, content.find, frontmatter_raw, body) to first normalize
content (e.g., replace "\r\n" with "\n") or use a regex/split that accepts
"\r?\n" so name/tools/model/skills metadata are correctly extracted regardless
of CRLF vs LF.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 50c86e0e-77e2-4a75-a6f4-a4722275b564

📥 Commits

Reviewing files that changed from the base of the PR and between 30666a6 and ee18cd1.

📒 Files selected for processing (3)
  • components/runners/ambient-runner/ambient_runner/bridges/claude/agents.py
  • components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py
  • components/runners/ambient-runner/tests/test_custom_agents.py

@mcljot mcljot marked this pull request as draft April 4, 2026 18:49
@mcljot mcljot changed the title Feature/custom subagent loading feat(custom-agents): add filesystem-based loading of Claude subagents from .claude/agents/*.md Apr 4, 2026
@mcljot mcljot closed this Apr 4, 2026
@mcljot mcljot reopened this Apr 4, 2026
Re-raise OSError from _parse_agent_file so unreadable files are skipped
instead of silently creating agents with empty prompts. Normalise CRLF
to LF before frontmatter parsing so Windows-origin files work correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mcljot mcljot marked this pull request as ready for review April 4, 2026 21:16
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/runners/ambient-runner/ambient_runner/bridges/claude/agents.py`:
- Around line 106-113: Resolve and validate the agent name before inserting into
the agents dict: retrieve and trim the name (replace name = metadata.get("name",
md_file.stem) with something like resolved_name = (metadata.get("name") or
md_file.stem).strip()), ensure resolved_name is not empty (fallback to
md_file.stem if it is), and then check agents for an existing key (e.g., if
resolved_name in agents) and either raise/return an explicit error or log and
skip to avoid silent overwrites; finally use resolved_name when constructing the
AgentDefinition and inserting into agents.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ea53e688-2d04-4798-83d7-55b280a0c795

📥 Commits

Reviewing files that changed from the base of the PR and between ee18cd1 and 0ae47c2.

📒 Files selected for processing (1)
  • components/runners/ambient-runner/ambient_runner/bridges/claude/agents.py

Comment on lines +106 to +113
name = metadata.get("name", md_file.stem)
description = metadata.get("description", f"Agent: {name}")
tools = _parse_string_list(metadata.get("tools"))
skills = _parse_string_list(metadata.get("skills"))
model = metadata.get("model")

agents[name] = AgentDefinition(
description=description,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate resolved agent names and prevent silent overwrites.

At Line 106, an empty frontmatter name (e.g., name:) is accepted, and at Line 112 duplicate names overwrite previously loaded agents without warning. This can silently drop intended subagent registrations.

Suggested fix
-            name = metadata.get("name", md_file.stem)
+            name = (metadata.get("name") or md_file.stem).strip()
+            if not name:
+                logger.warning(
+                    "Skipping agent from %s: resolved name is empty", md_file.name
+                )
+                continue
+            if name in agents:
+                logger.warning(
+                    "Skipping duplicate custom agent '%s' from %s",
+                    name,
+                    md_file.name,
+                )
+                continue
             description = metadata.get("description", f"Agent: {name}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/runners/ambient-runner/ambient_runner/bridges/claude/agents.py`
around lines 106 - 113, Resolve and validate the agent name before inserting
into the agents dict: retrieve and trim the name (replace name =
metadata.get("name", md_file.stem) with something like resolved_name =
(metadata.get("name") or md_file.stem).strip()), ensure resolved_name is not
empty (fallback to md_file.stem if it is), and then check agents for an existing
key (e.g., if resolved_name in agents) and either raise/return an explicit error
or log and skip to avoid silent overwrites; finally use resolved_name when
constructing the AgentDefinition and inserting into agents.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant