Skip to content

fix(claude-agent-sdk): load skills natively and stop inheriting ambient ones - #354

Merged
jrob5756 merged 3 commits into
mainfrom
fix/352-native-skills
Aug 3, 2026
Merged

fix(claude-agent-sdk): load skills natively and stop inheriting ambient ones#354
jrob5756 merged 3 commits into
mainfrom
fix/352-native-skills

Conversation

@jrob5756

Copy link
Copy Markdown
Collaborator

Closes #352

What

claude_agent_sdk.py declared supports_native_skills=False on the grounds that the upstream SDK had no skill surface. That is out of date, and expensive: the full SKILL.md plus the entire references/ tree was injected into every rendered prompt, on every call, every retry, and every validator pass (~27K tokens for the bundled conductor skill).

It also never set setting_sources, so the claude CLI discovered and enabled skills from ~/.claude/skills/, every .claude/skills/ up the directory tree, and enabled plugins — none of which the workflow declared, all of which varied by developer machine and launch directory. Conductor documents skills: [] as an explicit opt-out; on this provider it opted out of nothing.

Same class of hole strict_mcp_config=True already closes for MCP servers, a few lines away in the same file.

How

  • Native loading — the Claude Code plugin owning each skill is registered via ClaudeAgentOptions.plugins and the skill enabled by its <plugin>:<skill> name, so the CLI reads only the SKILL.md frontmatter up front and loads the body on demand. _resolve_skill_plugins maps each resolved skill directory back to its plugin (skills/registry.py::resolve_skill_plugin walks up for .claude-plugin/plugin.json); a directory with no manifest raises ProviderError rather than being silently dropped.
  • setting_sources=[] unconditionally — the skills counterpart to the unconditional strict_mcp_config. Note skills=[] and skills=None are not interchangeable upstream: None means "CLI defaults apply", so only the explicit empty list makes the opt-out real.
  • tools: [] + skills now grants back the single Skill tool. An explicit tools: [] sends --tools "" (empty base tool set), which would otherwise leave the declared skill unreachable. No permission bypass needed — the SDK auto-allows it via Skill(<name>) in allowed_tools.
  • Packaging fix (not in the issue) — the wheel force-include shipped only the skill body, not plugins/conductor/.claude-plugin/. Without the manifest, a non-editable install resolved a plugin root the CLI cannot load. Verified by building the wheel and resolving the skill from an actual wheel install.

⚠️ Behaviour change

Agents on this provider no longer inherit ambient CLAUDE.md, .claude/rules/*.md, project settings.json, or hooks. Use --workspace-instructions / --instructions to supply that content explicitly. Called out in the CHANGELOG.

Verification

The SDK surface was checked against the real installed package at the pin floor 0.2.82 as well as current 0.2.128skills, setting_sources, and plugins all exist at both, so no pin bump is needed.

Tests assert the actual argv the SDK builds rather than stopping at the options object, across all four skills x tools combinations:

Case argv
skills, tools: [] --tools Skill --allowedTools Skill(conductor:conductor) --setting-sources= --plugin-dir <root>
skills, tools omitted --tools default --allowedTools Skill(conductor:conductor) --setting-sources= --plugin-dir <root>
no skills, tools: [] --tools "" --setting-sources= (no --plugin-dir)
no skills, tools omitted --tools default --setting-sources= (no --plugin-dir)
Check Result
Full suite with claude-agent-sdk extra (ci.yml:109) 4731 passed
Full suite without the extra (default CI job) 4580 passed
make check (ruff + ty, CI config) clean
make validate-examples exit 0
Wheel build + install manifest packaged, conductor:conductor resolves

Review notes

  • The one thing not verifiable offline is whether the CLI wants the qualified conductor:conductor or the bare conductor. We send the documented plugin-qualified form (and the repo's own local-dev flow is claude --plugin-dir plugins/conductor). It is a one-line change in _resolve_skill_plugins if a live run says otherwise.
  • Draft: worth a live smoke test against a real claude CLI before merging.
  • Unrelated stale doc spotted and left alone: references/yaml-schema.md still says mcp_servers is "ignored by claude-agent-sdk", which claude-agent-sdk provider: implement MCP server translation (mcp_tools=False is a gap, not an SDK limit) #335 made false. Worth a follow-up.

@jrob5756

Copy link
Copy Markdown
Collaborator Author

Addressed review feedback in d6a70ed. Seven review passes; no blocking bugs, but several findings converged on one theme: the first pass was loud about the single case where it couldn't build a skill name, and silent about every case where the name it built was wrong — the same failure mode #352 exists to fix.

Resolution now refuses, with the real reason attached

  • A manifest found above a skill no longer makes that plugin its owner — the skill must live under the candidate's skills/, or the walk continues.
  • SKILL.md must exist and its frontmatter name must equal the directory name. The CLI resolves by frontmatter name while we send the directory name, so drift hides the skill rather than failing.
  • Names restricted to [A-Za-z0-9_.-]+ — they're joined into a comma-delimited --allowedTools value, where , or : splits into extra permission rules.
  • Two plugins claiming one qualified name are refused, not deduped (deduping drops a declared skill).
  • resolve_skill_plugin returns None only for "no owning plugin root" and raises SkillPluginError when a plugin is present but unusable, so a broken manifest stops being reported as a missing one.
  • All resulting ProviderErrors set is_retryable=False, matching sibling config errors — the message interpolates a filesystem path, and the default heuristic sniffs for "connection"/"timeout".

Test gap that mattered most

The executor→provider seam had zero coverage. Four mutations there — including skill_directories=None — suppressed every skill with the entire suite green. Eager injection used to backstop that seam; this PR removed the backstop. Now covered, and I verified all three previously-silent regressions fail under mutation:

Mutation Before After
skill_directories=None in executor green fails
supports_native_skills check removed green fails
SKILL.md frontmatter name renamed green fails

Docs corrections — several claims were wrong, including mine:

  • A missing wheel manifest fails loudly; it does not "silently resolve to nothing".
  • The skills: [] opt-out needs setting_sources=[] and an explicit skills list — the SDK re-defaults setting_sources to ["user","project"] when skills is set without it. The changelog credited only one, the source comments only the other; either could have been "simplified" away, re-opening the issue with tests green.
  • The skill list is a context filter, not a sandbox — undeclared skills are hidden from the listing, but their files stay readable.
  • docs/providers/comparison.md had no mention of skills or the settings isolation at all; a user's only notice that CLAUDE.md stopped loading was the changelog.

A confirmation pass then caught one bug introduced by the fixes: SkillPlugin.__post_init__ raised a bare ValueError that slipped past the provider's except SkillPluginError. Fixed and regression-tested.

Verification: 4760 passed with the claude-agent-sdk extra / 4606 without (+29 and +26), make check clean in CI's config, make validate-examples exit 0.

Still draft — the qualified conductor:conductor vs bare conductor name remains the one thing not verifiable without a live claude CLI.

Jason Robert and others added 3 commits August 3, 2026 14:41
…nt ones

The provider declared supports_native_skills=False on the grounds that the
upstream SDK had no skill surface. That is out of date, and expensive: the
full SKILL.md plus the entire references/ tree was injected into every
rendered prompt, on every call, retry, and validator pass.

It also never set setting_sources, so the claude CLI discovered and enabled
skills from ~/.claude/skills, every .claude/skills up the directory tree,
and enabled plugins - none declared by the workflow, all varying by machine
and launch directory. Conductor documents `skills: []` as an explicit
opt-out; on this provider it opted out of nothing.

- Register the Claude Code plugin owning each skill via
  ClaudeAgentOptions.plugins and enable it by its <plugin>:<skill> name, so
  the CLI reads only the frontmatter up front and loads the body on demand.
- Set setting_sources=[] unconditionally, the skills counterpart to the
  unconditional strict_mcp_config. Note skills=[] and skills=None are not
  interchangeable upstream: None means "CLI defaults apply", so only the
  explicit empty list makes the opt-out real.
- Grant back the single Skill tool for an explicit `tools: []` when skills
  are enabled, since an empty base tool set would otherwise leave the
  declared skill unreachable.
- Ship plugins/conductor/.claude-plugin/ in the wheel. Only the skill body
  was packaged, so a non-editable install resolved a plugin root the CLI
  could not load.

Behaviour change: agents on this provider no longer inherit ambient
CLAUDE.md, .claude/rules/*.md, project settings.json, or hooks. Use
--workspace-instructions to supply that content explicitly.

Tests assert the actual argv the SDK builds across all four skills x tools
combinations rather than stopping at the options object.

Closes #352

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…hing

Review follow-up. The first pass was loud about the one case where it
could not build a skill name, and silent about every case where the name
it built was wrong - which is the same failure #352 exists to fix.

Resolution now refuses, with the real reason attached:

- A plugin manifest found above a skill no longer makes that plugin its
  owner; the skill must live under the candidate's skills/ directory, or
  the walk continues.
- SKILL.md must exist and its frontmatter name must equal the directory
  name. The CLI resolves enabled skills by frontmatter name while we send
  the directory name, so drift hides the skill rather than failing.
- Names are restricted to [A-Za-z0-9_.-]+. They are joined into a
  comma-delimited --allowedTools value, where a ',' or ':' would split
  into extra permission rules.
- Two plugins claiming one qualified name are refused rather than deduped,
  since deduping drops a declared skill.
- resolve_skill_plugin returns None only for "no owning plugin root" and
  raises SkillPluginError when a plugin is present but unusable, so the
  provider stops reporting a missing manifest for a broken one.
- Every resulting ProviderError sets is_retryable=False, matching the
  sibling config errors: the message interpolates a filesystem path, and
  the default heuristic sniffs messages for "connection" / "timeout".

Tests: the executor -> provider seam had no coverage, so four mutations
in it (including skill_directories=None) suppressed every skill with the
suite green. That seam and the frontmatter/directory pin are now asserted;
both were verified to fail under mutation.

Docs: several claims were wrong. A missing wheel manifest fails loudly
rather than silently resolving to nothing; the skills: [] opt-out needs
setting_sources=[] *and* an explicit skills list, because the SDK
re-defaults setting_sources when skills is set without it; and the skill
list is a context filter, not a sandbox. Also updates the user-facing
provider docs, which had no mention of skills or the settings isolation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ls change

Rebasing onto #348 left three statements that were true when written and
are not any more. Each told a reader that cwd drags the CLI's ambient
instructions, settings, and hooks in, which the unconditional
setting_sources=[] now prevents -- and AGENTS.md said outright that
Conductor never sets setting_sources, three lines above the bullet
explaining why it always does.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jrob5756
jrob5756 force-pushed the fix/352-native-skills branch from d6a70ed to 3c82f44 Compare August 3, 2026 18:48
@jrob5756
jrob5756 marked this pull request as ready for review August 3, 2026 18:49
@jrob5756

jrob5756 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main to pick up #349 (working_dir via ClaudeAgentOptions.cwd), which had landed in the meantime and touched the same four files.

The two changes are independent — cwd versus skills/setting_sources/plugins — so the conflicts were mechanical: both sides of the CAPABILITIES block, the options construction, the AGENTS.md parity notes, and two test classes that git had interleaved because both were appended at the end of the file. Both TestWorkingDirectory and TestSkillsWiring are intact.

Worth flagging one thing the rebase surfaced that a clean auto-merge would have hidden: #348 documented, correctly at the time, that cwd drags the CLI's ambient CLAUDE.md, settings, and hooks along with it — including the line "Conductor never sets setting_sources, so the CLI's load-everything default applies." This PR makes that false. After the rebase, AGENTS.md asserted it three lines above the bullet explaining why setting_sources is now always []. Corrected in AGENTS.md, CHANGELOG.md, and docs/providers/experimental.md.

The two features compose rather than collide: cwd still selects the transcript directory and where a project .mcp.json/.claude/ tree would be looked for, while strict_mcp_config and setting_sources=[] stop anything there being loaded. "Point working_dir only at trees you trust" is no longer the caveat it was.

Verification on the rebased tip: 4773 passed with the claude-agent-sdk extra / 4608 without, make check clean, make validate-examples exit 0.

Marking ready for review.

@jrob5756
jrob5756 merged commit 96bdf6b into main Aug 3, 2026
10 checks passed
@jrob5756
jrob5756 deleted the fix/352-native-skills branch August 3, 2026 18:53
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.

claude-agent-sdk: use native skills option, and stop loading ambient skills implicitly

1 participant