9.3 KiB
Session Skills — Design
Date: 2026-07-03 Status: Approved (design), implementation not started
Problem
Mika wants to give headless task agents a specific Claude skill (e.g.
ponytail) without installing it
globally in ~/.claude/skills/, where it would leak into every interactive session.
Skills should be a first-class, per-level session setting alongside model,
max_turns, and system_prompt — configurable global / per-list / per-task — and
sourced from a GitHub URL.
Key facts that shape the design
- The Claude CLI discovers skills from two places: the global
~/.claude/skills/(every session — undesirable here) and the working directory's.claude/skills/(plus plugins). ClaudeDo fully controls each spawned session'sWorkingDirectory(ClaudeProcess.cs:30), so a skill dropped into the session cwd is scoped to exactly that headless run. - Auto-commit uses
git add -A(WorktreeManager.CommitIfChangedAsync→_git.AddAllAsync,WorktreeManager.cs:142). Anything seeded into a worktree's.claude/skills/would be committed unless explicitly excluded → the seeder must add the seeded paths to the worktree'sinfo/exclude. - A skill is not just prompt text:
SKILL.mdmay reference scripts that run via Bash. Headless agents run with--permission-mode auto(effectively unattended), so a skill pulled from an arbitrary URL is unattended third-party code execution. This is why install is a deliberate, pinned, reviewable step — not a live per-run URL fetch.
Decisions (locked)
- Install-and-pin, not live fetch. A dedicated registry screen installs a skill once: clone the repo, pin to the current commit, store locally. Per-level config then references installed skills by name (checkboxes), never a URL.
- Three levels, additive union. Effective skill set =
global ∪ list ∪ task. (Unlikemodel/prompt, which override — skills add up. Trade-off accepted: an inherited skill can't be switched off for a single task in the MVP.) - A repo can contribute multiple skills. Installer detects the layout:
skills/*/SKILL.md(plugin bundle) → import each subskill flat. This is ponytail: it ships 6 skills (ponytail,-help,-review,-audit,-debt,-gain) underskills/<name>/SKILL.mdplus.claude-plugin/, hooks, commands, an MCP — none of which we consume; we take only theskills/<name>/dirs.- root
SKILL.md→ single skill. - neither → reject.
The CLI expects
.claude/skills/<name>/SKILL.mdflat, so subskills are flattened on install. Selection is per individual skill name (enable justponytail+ponytail-helpif you want, not all six).
- Public repos only (plain
git cloneover HTTPS, no auth) for MVP.
Correction (2026-07-03, from the smoke test): the original "one repo = one skill at root" MVP was wrong for the very target repo — ponytail is a multi-skill plugin. Decision 3 above replaces it.
Architecture
Storage & registry
- Each discovered skill is copied flat to
~/.todo-app/session-skills/<name>/(its own self-contained dir withSKILL.mdat the root of that dir), so the seeder just copies<name>/→ cwd. - New DB table
session_skills, one row per skill (a multi-skill repo writes N rows sharingsource_url+pinned_ref):name(PK),source_url,pinned_ref(commit SHA),subpath(dir within the repo the skill came from, e.g.skills/ponytailor.for root),description,added_at. Repo-level ops act on all rows with the samesource_url(no separate sources table — keep it flat). - New worker service
SessionSkillRegistry(in a newSkills/area under the Worker):InstallAsync(url)— clone to temp → detect layout (skills/*/SKILL.mdbundle vs rootSKILL.md) → for each discovered skill parse frontmatter (name,description), resolve HEAD SHA aspinned_ref, copy its dir flat into place, upsert a row. Returns the list of installed skill names. Name collision (a skill name from a different source) → error surfaced to UI; reinstalling the same source updates. Clone is injected (IRepoCloner) so tests use a local source dir — no real network, no real CLI.UpdateAsync(sourceUrl)— re-clone, re-detect, refresh that source's skills +pinned_ref.RemoveAsync(sourceUrl)— delete all its skill dirs + rows.ListAsync()— registry entries for the UI (grouped by source for display).
Resolution
TaskRunner.ResolveConfigAsync (TaskRunner.cs:488) already merges
task → list → global for the other fields. Add:
SkillNames = Union(task.SessionSkills, listConfig?.SessionSkills, global.SessionSkills)
deduped, filtered to names that still exist in the registry (a removed skill is silently
dropped — logged). Add IReadOnlyList<string> SkillNames to ClaudeRunConfig
(ClaudeArgsBuilder.cs:5). No CLI flag is emitted — skills are seeded on disk, not
passed as args. ClaudeArgsBuilder.Build is unchanged for skills.
Per-level storage: a nullable TEXT column session_skills (JSON array of names) on
tasks, list_config, and app_settings.
Seeding
New service SessionSkillSeeder, called by TaskRunner after the working dir is
resolved and before ClaudeProcess.RunAsync:
- For each resolved skill name, copy
~/.todo-app/session-skills/<name>/→<cwd>/.claude/skills/<name>/(overwrite → idempotent for resume/re-run). - If the cwd is a git worktree, append
/.claude/skills/<name>/to the worktree'sinfo/exclude(path viagit rev-parse --git-path info/exclude, so it targets the per-worktree exclude), only if not already present. Only the seeded subdirs are excluded — never blanket-exclude/.claude/, in case the target project commits its own.claude/. - Sandbox runs (not a repo) skip the exclude step.
- No separate cleanup: seeded dirs vanish with the worktree/sandbox.
allowedTools caveat
--allowedTools is only emitted when set (ClaudeArgsBuilder.cs:80); normal task runs
leave it null → all tools allowed → the Skill tool is available. If a future per-task
allowedTools restriction is added, it must include Skill. Noted, not handled in MVP.
UI
Mirror the existing agent-file pattern.
- Registry screen ("extra mask"): a new Skills tab in the Settings modal
(
SettingsModalView.axaml) withSessionSkillsSettingsTabViewModel. Add (URL text box → install a repo, which may yield several skills); lists installed skills grouped by source (name, description, source, short ref); Update / Remove act per source (repo). Status/error line likeFilesSettingsTabViewModel. - Global selector: multi-select (checkbox list) of installed skills in the General
settings tab →
AppSettings.SessionSkills. - List + Task selectors: add a skills multi-select to the shared
AgentConfigEditorcontrol (AgentConfigEditor.axaml/AgentConfigEditorViewModel.cs), which is already reused by both List settings and the per-task flyout — one addition covers both levels, with the existing inheritance-badge pattern.
Hub / client surface
New WorkerHub methods + IWorkerClient entries (update hand-rolled fakes in both test
projects — see memory iworkerclient_fakes_sync):
GetSessionSkills, InstallSessionSkill(url), UpdateSessionSkill(sourceUrl),
RemoveSessionSkill(sourceUrl). Extend AppSettingsDto, ListConfigDto,
UpdateListConfigDto, UpdateTaskAgentSettingsDto with the selected skill-name lists.
New SessionSkillDto.
Edge cases
- Removed skill still referenced by a level → dropped at resolve time, logged, no failure.
- Name collision on install → reject with a clear message; offer Update instead.
- Repo without root
SKILL.md→ reject at install. - Target project already has
.claude/skills/→ additive copy; exclude only our subdirs. - Resume / re-run reuses the worktree → re-seed overwrites, exclude append is idempotent.
Verification (must-check, can't be unit-tested)
- Does
claude -pactually load and invoke a skill placed in cwd.claude/skills/? Load-bearing assumption. Smoke test 2026-07-03:system:initconfirms correctcwd+Skilltool present; one authed run engaged the ponytail skill (12 mentions, vs zero on an unauthed run). Clean repro is blocked by auth-token rotation in the bash sandbox (parallel-session contention, not a design issue) — Mika confirms with one run in a stable terminal:cd /tmp/claudedo-skill-smoke && echo "Use the ponytail-help skill to show the levels table, then stop." | claude -p --permission-mode acceptEdits --max-turns 4. If headless does not surface cwd skills, fall back toCLAUDE_CONFIG_DIRisolation (heavier — needs credentials copied) and revisit. - Seeded skill is not committed by the auto-commit step (worktree run).
- Skill does not appear in a normal interactive session (no global leak).
Out of scope (MVP)
Private-repo auth; consuming a plugin's non-skill parts (hooks, commands, MCP — we take
only skills/<name>/); auto-update & update notifications; per-task disabling of an
inherited skill; surfacing skill invocation in the run log.