7.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.) - One repo = one skill. Installer accepts only a repo with
SKILL.mdat the root (covers ponytail). Multi-skill repos / plugin-format repos are out of scope for MVP. - Public repos only (plain
git cloneover HTTPS, no auth) for MVP.
Architecture
Storage & registry
- Installed skills live at
~/.todo-app/session-skills/<name>/(the cloned repo contents,SKILL.mdat root). - New DB table
session_skills:name(PK),source_url,pinned_ref(commit SHA),description,added_at. - New worker service
SessionSkillRegistry(in a newSkills/area under the Worker):InstallAsync(url)— clone to temp → validateSKILL.mdat root → parse frontmatter (name,description) → resolve HEAD SHA aspinned_ref→ move into place → upsert DB row. Name collision → error surfaced to UI. The clone step is injected (IRepoCloner) so tests use a local source dir — no real network, no real CLI.UpdateAsync(name)— re-fetch, checkout latest, refresh files +pinned_ref.RemoveAsync(name)— delete dir + row.ListAsync()— registry entries for the UI.
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. Lists installed skills (name, description, source, short ref); Add (URL text box → install), Update, Remove. 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(name),
RemoveSessionSkill(name). 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/? This is the load-bearing assumption. Manual smoke test with ponytail before calling the feature done. If headless mode 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; multi-skill / plugin-format repos; auto-update & update notifications; per-task disabling of an inherited skill; surfacing skill invocation in the run log.