Skip to content

feat(summon): make skill supporting files individually loadable via load()#7583

Merged
wpfleger96 merged 6 commits intomainfrom
wpfleger/summon
Mar 9, 2026
Merged

feat(summon): make skill supporting files individually loadable via load()#7583
wpfleger96 merged 6 commits intomainfrom
wpfleger/summon

Conversation

@wpfleger96
Copy link
Copy Markdown
Collaborator

@wpfleger96 wpfleger96 commented Feb 27, 2026

Summary

  • Supporting files in a skill directory are now individually loadable via load(source: "skill-name/relative/path") — e.g. load(source: "oncall-buddy-help/references/technical-ops.md")
  • Loading a skill (load(source: "skill-name")) now lists available supporting files with their load() names rather than inlining their content
  • All file types (markdown, scripts, configs) are loadable this way, not just .md files
  • Path traversal protection: canonicalization check prevents symlinks from reading files outside the skill directory
  • When a namespaced path isn't found, the error message lists available files for that skill

Problem

Skill environments without filesystem access (e.g. Block-internal Goose Slackbot deployments) had no way to reach supporting reference files. The previous approach — listing paths with "Use the file tools to read these files" — only worked for environments with the developer extension.

Behavior

load(source: "oncall-buddy-help")

Now outputs:

## Supporting Files

Skill directory: ~/.claude/skills/oncall-buddy-help

The following supporting files are available:
- references/technical-ops.md → load(source: "oncall-buddy-help/references/technical-ops.md")
- references/diagnostics.md → load(source: "oncall-buddy-help/references/diagnostics.md")

Use load(source: "<skill-name>/<path>") to load individual files into context, or use file tools to read/run them directly.

The LLM can then selectively load only the reference it needs, preserving progressive disclosure while remaining fully functional in filesystem-restricted environments.

@wpfleger96 wpfleger96 marked this pull request as ready for review February 27, 2026 23:51
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ec8e9fd48b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ec435d83a4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@tlongwell-block
Copy link
Copy Markdown
Collaborator

I think #7457 might help here?

@wpfleger96
Copy link
Copy Markdown
Collaborator Author

@tlongwell-block so #7457 enables the Slackbot to see that the skill's supplemental/reference material exists, but it's still unreachable/unusable

the skill is structured like:

oncall-buddy-help
├── references
│   ├── diagnostics.md
│   ├── hints-template.md
│   └── technical-ops.md
└── SKILL.md

and the agent can load(source: oncall-buddy-help), which loads only the content from SKILL.md. to be able to load the skill's reference content for deeper debugging, it needs to be able to do:

load(source: references/technical-ops)

and the model does try this, but it fails with:

Error: Source 'references/technical-ops' not found. Use load() to see available sources.

because currently load() only loads the root level SKILL.md content and for any supplemental content, examples, scripts, etc. the model would need a shell filesystem access to read the file contents directly which the Slackbot can't do.

if we're going to abstract the SKILL.md file away by calling load(source: <skill_name>) then I'm thinking it makes sense to do the same for skill reference files/supplemental content instead of having a dual path of load() vs raw file read

@michaelneale
Copy link
Copy Markdown
Collaborator

I think this is ok @wpfleger96 - downside is that this is less "progressive loading" which is one of the tenets of skills. So downside is if there are skills which are mountains and mountains of .md files, it will blow out context.

tagging @tlongwell-block who would be aware of this limitation and may have thoughts. I think it is ok but I don't know how bloated skills are (the ones I have seen in squareup/agents I think may end up a bit bloated, but I guess you only load one or two).

@wpfleger96 wpfleger96 changed the title feat(summon): auto-include markdown supporting files when loading skills feat(summon): make skill supporting files individually loadable via load() Mar 3, 2026
@wpfleger96
Copy link
Copy Markdown
Collaborator Author

@michaelneale @tlongwell-block 🙃 sorry I totally botched this PR, my intention was to preserve progressive disclosure here not break it!

I just pushed a new change to address this - now instead of inlining supporting files on load, each supporting file is now individually addressable via load():

load(source: "oncall-buddy-help/references/technical-ops.md")

Loading the base skill still just loads SKILL.md and lists available supporting files with their load() names so the LLM knows how to reach them when needed:

## Supporting Files

The following supporting files are available:
- references/technical-ops.md → load(source: "oncall-buddy-help/references/technical-ops.md")
- references/diagnostics.md → load(source: "oncall-buddy-help/references/diagnostics.md")

Use load(source: "<skill-name>/<path>") to load individual files into context...

My intention here was to make skill supplemental files reachable for geese without filesystem access, but not break skill progressive disclosure! What do y'all think

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d784c78378

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@wpfleger96 wpfleger96 force-pushed the wpfleger/summon branch 3 times, most recently from 6ff5592 to 8afe5cd Compare March 3, 2026 15:25
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8afe5cd747

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ec87472f52

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Supporting files were only listed as paths with "use file tools to read
these" — but environments without the developer extension (e.g. slackbots)
have no file-reading tools, making reference files unreachable. Now .md/.mdx
supporting files are read and included inline alongside the SKILL.md content,
consistent with how SKILL.md itself is already read and included. Non-markdown
files continue to be listed as paths.
… auto-inlining

The previous approach read all markdown supporting files into context
on every load(source: "skill-name") call, breaking progressive disclosure
— a core skill design tenet. Skills like oncall-buddy-help with large
reference directories would blow out context on every load.

The real problem: environments without filesystem access (Slackbot)
couldn't reach supporting files at all since they had no tools to read
them directly. The fix is to make each supporting file individually
loadable via load(source: "skill-name/relative/path") rather than
dumping everything upfront.

load(source: "skill-name") now lists supporting files with their load()
names so the LLM knows how to progressively pull in what it needs.
Includes symlink safety check (canonicalize + starts_with) to prevent
path traversal via malicious skill directories.
…file resolution

On Windows, Path::to_string_lossy() uses backslash separators but
load() paths use forward slashes. Normalize discovered file paths
to use forward slashes before comparison and in user-facing output
so load(source: "skill/references/ops.md") matches consistently.
When a matched supporting file can't be read (permissions, non-UTF8),
resolve_source now returns an error describing the actual failure
instead of returning None which surfaced as a misleading "not found"
message. Changed the return type to Result<Option<Source>, String>
to distinguish "not found" from "found but failed to read".
@DOsinga DOsinga self-assigned this Mar 9, 2026
Fixes TOCTOU race in path traversal check by reading from the
canonicalized path instead of the original, eliminating the window
for symlink swaps between check and read. Also distinguishes
canonicalize failures ("file not found") from traversal violations
("resolves outside skill directory") in error messages.

Adds kind filter to error path so a recipe sharing a skill's name
doesn't produce "Skill 'x' has no supporting files". Blocks
delegate() on supporting file paths since raw file content shouldn't
be used as recipe instructions. Validates skill names don't contain
'/' during parsing to prevent ambiguous resolution. Normalizes
user-supplied relative paths for Windows compatibility. Caps the
available-files error list at 10 entries.

Adds security tests for symlink traversal (#[cfg(unix)]), path
traversal input, and slash-in-name rejection.
@tlongwell-block
Copy link
Copy Markdown
Collaborator

/goose

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 9, 2026

Summary: This PR enables individual loading of skill supporting files via load(source: "skill-name/path") for environments without filesystem access. The implementation is well-designed with proper security controls (canonicalization, known-file-only matching, slash rejection in skill names) and excellent test coverage including symlink attack prevention.

🟡 Warnings

  1. Canonicalization fallback may weaken security check (summon.rs:720-723)
    let canonical_skill_dir = skill
        .path
        .canonicalize()
        .unwrap_or_else(|_| skill.path.clone());
    If canonicalize() fails (e.g., skill directory has weird permissions but files are still readable), the fallback to the non-canonical path could theoretically weaken the starts_with security check if file_path.canonicalize() succeeds and returns a different prefix. Consider returning an error instead of falling back:
    let canonical_skill_dir = skill
        .path
        .canonicalize()
        .map_err(|e| format!("Cannot verify skill directory '{}': {}", skill.name, e))?;
    This is a low-probability scenario but a stricter approach would be more defensive.

🟢 Suggestions

  1. Minor: Check for supporting file path early in delegate (summon.rs:1341-1347)
    The check if source_name.contains('/') happens after resolve_source has already done the work of resolving the supporting file. Moving this check before resolve_source would avoid unnecessary work, though the performance impact is negligible:
    // Could check early to avoid unnecessary resolution
    if source_name.contains('/') {
        return Err(format!(
            "Cannot delegate to supporting file '{}'. Use load() to read it instead.",
            source_name
        ));
    }

✅ Highlights

  1. Excellent security design: The path traversal defense is well-layered:

    • Only iterates through pre-discovered supporting_files (not arbitrary paths)
    • Exact match required (rel_normalized == relative_path)
    • Canonicalization check catches symlink attacks
    • Skill names with / are rejected during parsing (prevents bad/skill from enabling namespace confusion)
  2. Comprehensive test coverage: Six new tests covering:

    • Load names displayed correctly
    • Supporting file loading by path
    • Error messages listing available files
    • Symlink attack blocking (unix-specific)
    • Path traversal input rejection
    • Skill name with / rejection
  3. Good error messages: When a namespaced path isn't found, the error lists available files (up to 10), making the UX much better for debugging.

  4. Progressive disclosure preserved: The PR description correctly identifies that auto-inlining all files defeats the skill design. Listing files with load() names maintains progressive disclosure while solving the no-filesystem-access use case.


Review generated by goose

@wpfleger96 wpfleger96 added this pull request to the merge queue Mar 9, 2026
Merged via the queue into main with commit c1d8e27 Mar 9, 2026
21 checks passed
@wpfleger96 wpfleger96 deleted the wpfleger/summon branch March 9, 2026 22:17
wpfleger96 added a commit that referenced this pull request Mar 9, 2026
* origin/main:
  feat(summon): make skill supporting files individually loadable via load() (#7583)
  Keep toast open on failed extension (#7771)
wpfleger96 added a commit that referenced this pull request Mar 9, 2026
…e-issue

* origin/main:
  feat(summon): make skill supporting files individually loadable via load() (#7583)
  Keep toast open on failed extension (#7771)
lifeizhou-ap added a commit that referenced this pull request Mar 11, 2026
* main: (45 commits)
  fix: resolve {{ recipe_dir }} in nested sub-recipe paths during secret discovery (#7797)
  Add @DOsinga as CODEOWNER for documentation (#7799)
  feat: Add summarize tool for deterministic reads (#7054)
  fix(api): use camelCase in CallToolResponse and add type discriminators to ContentBlock (#7487)
  feat: ACP providers for claude code and codex (#6605)
  chore(deps): bump express-rate-limit from 8.2.1 to 8.3.0 in /evals/open-model-gym/mcp-harness (#7703)
  feat(openai): capture reasoning summaries from responses API (#7375)
  Fix some dependencies (#7794)
  fix: improve keyring availability error detection (#7766)
  feat: add MiniMax provider with Anthropic-compatible API (#7640)
  feat: add Tensorix as a declarative provider (#7712)
  fix(security): remove insecure default secret from GOOSE_EXTERNAL_BACKEND (#7783)
  refactor: Convert Tanzu provider to declarative JSON config (#7124)
  replaces https://github.com/block/goose/pull/7340/changes (#7786)
  feat(summon): make skill supporting files individually loadable via load() (#7583)
  Keep toast open on failed extension (#7771)
  fix(ui-desktop): unify path resolution around GOOSE_PATH_ROOT (#7335)
  fix: pass OAuth scopes to DCR and extract granted_scopes from token response (#7571)
  fix: write to real file if config.yaml is symlink (#7669)
  fix: preserve pairings when stopping gateway (#7733)
  ...
wpfleger96 added a commit that referenced this pull request Mar 11, 2026
…e-issue

* origin/main:
  feat(summon): make skill supporting files individually loadable via load() (#7583)
  Keep toast open on failed extension (#7771)
lifeizhou-ap added a commit that referenced this pull request Mar 11, 2026
* main: (69 commits)
  fix: resolve {{ recipe_dir }} in nested sub-recipe paths during secret discovery (#7797)
  Add @DOsinga as CODEOWNER for documentation (#7799)
  feat: Add summarize tool for deterministic reads (#7054)
  fix(api): use camelCase in CallToolResponse and add type discriminators to ContentBlock (#7487)
  feat: ACP providers for claude code and codex (#6605)
  chore(deps): bump express-rate-limit from 8.2.1 to 8.3.0 in /evals/open-model-gym/mcp-harness (#7703)
  feat(openai): capture reasoning summaries from responses API (#7375)
  Fix some dependencies (#7794)
  fix: improve keyring availability error detection (#7766)
  feat: add MiniMax provider with Anthropic-compatible API (#7640)
  feat: add Tensorix as a declarative provider (#7712)
  fix(security): remove insecure default secret from GOOSE_EXTERNAL_BACKEND (#7783)
  refactor: Convert Tanzu provider to declarative JSON config (#7124)
  replaces https://github.com/block/goose/pull/7340/changes (#7786)
  feat(summon): make skill supporting files individually loadable via load() (#7583)
  Keep toast open on failed extension (#7771)
  fix(ui-desktop): unify path resolution around GOOSE_PATH_ROOT (#7335)
  fix: pass OAuth scopes to DCR and extract granted_scopes from token response (#7571)
  fix: write to real file if config.yaml is symlink (#7669)
  fix: preserve pairings when stopping gateway (#7733)
  ...
wpfleger96 added a commit that referenced this pull request Mar 12, 2026
…e-issue

* origin/main:
  feat(summon): make skill supporting files individually loadable via load() (#7583)
  Keep toast open on failed extension (#7771)
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.

4 participants