Skip to content

feat: link agent skills into environments on activation - #597

Closed
joahg wants to merge 2 commits into
cashapp:masterfrom
joahg:joah/agent-skills
Closed

feat: link agent skills into environments on activation#597
joahg wants to merge 2 commits into
cashapp:masterfrom
joahg:joah/agent-skills

Conversation

@joahg

@joahg joahg commented Aug 4, 2026

Copy link
Copy Markdown

What

Environments can declare agent skillsSKILL.md directories used by AI coding agents (the .agents/skills / .claude/skills ecosystem conventions) — in bin/hermit.hcl, and Hermit provides them to the project the same way it provides toolchains:

skill-repo "https://github.com/org/repo.git" {
  path   = "skills"
  skills = ["my-skill"]
  # ref  = "<full commit sha>"   # optional security pin
}

On activation (both the activate command and hermit env --activate) each repository is resolved to a commit — the pinned SHA, or the remote HEAD re-checked at most every 15 minutes — and each declared skill is materialised as an immutable, content-addressed snapshot under the Hermit state directory, then symlinked into .agents/skills/<name> and .claude/skills/<name>.

Why

Today skills must be copied into every repository that wants them, and they drift. Declaring them in the environment gives every workflow that enters the project — humans, CI, coding agents — the same skills, with one committed line of provenance and no vendored content.

Design notes

  • Snapshots, not live checkouts. Project symlinks only ever point at immutable <state>/agent-skills/snapshots/<name>@<sha12>/ directories, never at a mutable checkout, so an update can never swap content under a running agent. Snapshot creation is atomic (temp dir + rename) and serialized by a file lock.
  • Persistent, activation-reconciled links. Like bin/ toolchain links, skill links persist; discovery is cwd-scoped so leaving the project "unloads" them. Reconciliation uses a per-environment ownership ledger: links Hermit created are updated/removed to match the declaration; repo-committed skill directories and foreign symlinks are never touched.
  • Never blocks the shell. Remote resolution is bounded by a 10s timeout and a 15-minute freshness window; when offline, activation degrades to the last good snapshot with a warning. Skill failures warn rather than fail activation.
  • Trust posture. Skill names are validated (^[a-z0-9][a-z0-9-]{0,63}$), path cannot escape the repository, symlinked skill paths that resolve outside the checkout are rejected, symlinks inside skill content are not copied, and ref pins must be full commit SHAs. Skill content never enters the project's version control; Hermit warns when the link directories are not gitignored.

Testing

  • go test ./... green; new agentskills package has unit coverage for linking, reconciliation, precedence (repo-committed skills win), pinned refs, HEAD updates, freshness window, offline fallback, failed-repo link preservation, and symlink-escape rejection.

joahg and others added 2 commits August 4, 2026 14:53
Environments can now declare agent skills — SKILL.md directories used by
AI coding agents — in bin/hermit.hcl, and Hermit provides them to the
project the same way it provides toolchains:

    skill-repo "https://github.com/org/repo.git" {
      path   = "skills"
      skills = ["my-skill"]
      # ref  = "<full commit sha>"   # optional security pin
    }

On activation (both the activate command and 'hermit env --activate')
each repository is resolved to a commit — the pinned SHA, or the remote
HEAD re-checked at most every 15 minutes — and each declared skill is
materialised as an immutable, content-addressed snapshot under the
Hermit state directory, then symlinked into .agents/skills/<name> and
.claude/skills/<name>.

Skill content never enters the project's version control: only the
hermit.hcl declaration is committed, and Hermit warns when the link
directories are not gitignored. Reconciliation is self-healing and
conservative: links for undeclared skills are removed via a per-env
ownership ledger, repo-committed skill directories and foreign symlinks
are never touched, and an unreachable remote degrades to the last good
snapshot instead of blocking activation or tearing down working links.

Buzz-Message: buzz://message?channel=573ff355-6c19-422d-b425-112853d0ec7e&id=39bb545df8409daa9df623a016f92a1da98bac803d0e370417a65235c208f327
Amp-Thread-ID: https://ampcode.com/threads/T-019fce44-560f-770d-bacb-fba12dd9ae31
Co-authored-by: Amp <amp@ampcode.com>
The skill source path is repository-controlled and was checked with
os.Stat, which follows symlinks: a repository could commit its skills
directory (or a skill entry) as a symlink to a local path outside the
checkout, and the snapshot would materialise that local content. The
OnSymlink skip policy only covers symlinks inside the copied tree, not
a symlinked source path.

Resolve the selected skill path and require it to remain within the
checkout before snapshotting.

Buzz-Message: buzz://message?channel=573ff355-6c19-422d-b425-112853d0ec7e&id=b5be8f34bb7e24316b8be6171fcb3089eeb8f9c8e0240cd7e0d5441cb503c8a7
Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019fce27-e59e-727c-b6d3-cdeeba894a4e
@joahg
joahg marked this pull request as ready for review August 4, 2026 20:11
@alecthomas

Copy link
Copy Markdown
Collaborator

This is definitely too specific to be in Hermit, but I'm interested in generalised solutions for the same pattern.

I think you could do the same by using on activate. You'd create an agent-skills package pointing at a git repo (already supported), with an on activate {...}  block that copies/symlinks the skills into the repo.

So I think all the pieces are already there, but give it a shot and see how it goes.

@alecthomas alecthomas closed this Aug 4, 2026
@joahg

joahg commented Aug 5, 2026

Copy link
Copy Markdown
Author

Gave the composition a shot with stock Hermit built from master — it works end to end: an env-local packages/agent-skills.hcl (git channel source with update = "15m", on activate symlinks) plus install-on-activate, and a fresh machine gets skills in one activation, channel refresh keeps them current, and offline degrades gracefully.

Five rough edges surfaced, all generic rather than skills-specific:

  1. validation rejects content-only packages (no binaries or apps provided), so a skills package needs a noop binary on PATH
  2. the symlink action isn't idempotent — second activation fails with file exists (its String() already advertises ln -sf); worked around with a recursive delete first, which relies on the undocumented action-type ordering in Trigger.Ordered
  3. channel update is only checked on stub exec, and a content package's binary is never exec'd, so it never refreshes — checking channels for install-on-activate packages during activation would fix it
  4. hermit env --activate doesn't run install-on-activate or activate triggers, so CI/non-shell flows don't get the links
  5. #ref maps to git clone --branch, so tags/branches can be pinned but not commit SHAs

Sending these as separate small PRs.

@joahg

joahg commented Aug 5, 2026

Copy link
Copy Markdown
Author

🤖 Sent by Joah's AI agent:

Follow-up PRs from the composition experiment above, each a small generic fix rather than anything skills-specific:

With those five, the agent-skills pattern works as a pure manifest convention on stock Hermit: a content-only package with a git channel source (update = "15m"), on activate symlinks into .agents/skills/.claude/skills, and install-on-activate.

@joahg

joahg commented Aug 5, 2026

Copy link
Copy Markdown
Author

🤖 Sent by Joah's AI agent:

@alecthomas the five PRs above (#598#602) are ready for your review whenever you have a chance — each is small, independent, and generic (no skills-specific behavior).

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.

2 participants