From 30083f32e124835672026b3045f9d9dbb502f95a Mon Sep 17 00:00:00 2001 From: Sai Karthik Date: Tue, 9 Jun 2026 21:56:11 +0530 Subject: [PATCH 1/5] cmd: add --no-skills, --skill, and --skills-dir CLI flags The pkg/kit Options struct already had full backend support for skills control (NoSkills, Skills []string, SkillsDir) wired into loadSkills() in pkg/kit/kit.go, but there were no corresponding CLI flags to drive them. This commit closes that gap. Changes in cmd/root.go: - Add three package-level flag variables alongside the existing noExtensionsFlag/extensionPaths group: noSkillsFlag bool skillsPaths []string skillsDir string - Register three persistent cobra flags in init(): --no-skills disable skill loading (auto-discovery and explicit) --skill load a skill file or directory (repeatable) --skills-dir override the project-local skills directory used for auto-discovery - Wire all three into the kitOpts struct literal in runNormalMode() so they flow directly into kit.New() -> loadSkills(). No changes to pkg/kit or internal/skills -- the backend was already complete. No viper binding is needed because kit.go reads these fields directly from opts rather than from viper (unlike NoExtensions which uses the viper fallback path). Example usage: kit --no-skills "prompt" kit --skill ./my-skill.md --skill ./other-skill.md "prompt" kit --skills-dir /path/to/skills "prompt" Co-authored-by: Claude --- cmd/root.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/cmd/root.go b/cmd/root.go index 99d81c63..cd4be842 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -74,6 +74,11 @@ var ( noCoreToolsFlag bool extensionPaths []string + // Skills control + noSkillsFlag bool + skillsPaths []string + skillsDir string + // TLS configuration tlsSkipVerify bool @@ -284,6 +289,14 @@ func init() { rootCmd.PersistentFlags(). StringSliceVarP(&extensionPaths, "extension", "e", nil, "load additional extension file(s)") + // Skills flags + rootCmd.PersistentFlags(). + BoolVar(&noSkillsFlag, "no-skills", false, "disable skill loading (auto-discovery and explicit)") + rootCmd.PersistentFlags(). + StringSliceVar(&skillsPaths, "skill", nil, "load skill file or directory (repeatable)") + rootCmd.PersistentFlags(). + StringVar(&skillsDir, "skills-dir", "", "override the project-local skills directory for auto-discovery") + flags := rootCmd.PersistentFlags() flags.StringVar(&providerURL, "provider-url", "", "base URL for the provider API (applies to OpenAI, Anthropic, Ollama, and Google)") flags.StringVar(&providerAPIKey, "provider-api-key", "", "API key for the provider (applies to OpenAI, Anthropic, and Google)") @@ -799,6 +812,9 @@ func runNormalMode(ctx context.Context) error { AutoCompact: autoCompactFlag, MCPAuthHandler: authHandler, DisableCoreTools: viper.GetBool("no-core-tools"), + NoSkills: noSkillsFlag, + Skills: skillsPaths, + SkillsDir: skillsDir, // This callback is called when each MCP server finishes loading. // We use a closure that captures appInstancePtr which is set after // app.New() is called below. From cf825c4158c3e3e3d176482e528d99c416064488 Mon Sep 17 00:00:00 2001 From: Sai Karthik Date: Tue, 9 Jun 2026 22:26:36 +0530 Subject: [PATCH 2/5] docs: document --no-skills, --skill, and --skills-dir CLI flags Add the three new skills CLI flags to all relevant documentation: - README.md: add Skills section under Global Flags CLI reference - www/pages/cli/flags.md: add Skills table (mirrors Extensions section pattern) - www/pages/cli/commands.md: expand the Skills section with usage examples and a description of auto-discovery vs explicit loading vs --no-skills Co-authored-by: Claude --- README.md | 5 +++++ www/pages/cli/commands.md | 20 ++++++++++++++++++++ www/pages/cli/flags.md | 8 ++++++++ 3 files changed, 33 insertions(+) diff --git a/README.md b/README.md index 9f8f354a..19689385 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,11 @@ mcpServers: --prompt-template Load a specific prompt template by name --no-prompt-templates Disable prompt template loading +# Skills +--skill Load skill file or directory (repeatable) +--skills-dir Override the project-local skills directory for auto-discovery +--no-skills Disable skill loading (auto-discovery and explicit) + # Generation parameters --max-tokens Maximum tokens in response (default: 8192, auto-raised up to 32768 for models with larger known output limits) --temperature Randomness 0.0-1.0 (default: 0.7) diff --git a/www/pages/cli/commands.md b/www/pages/cli/commands.md index 6cf17eb7..b46263bd 100644 --- a/www/pages/cli/commands.md +++ b/www/pages/cli/commands.md @@ -56,6 +56,26 @@ kit install --all # Install all extensions without prompting kit skill # Install the Kit extensions skill via skills.sh ``` +### Skills CLI flags + +Control which skills are loaded at startup: + +```bash +# Load a specific skill file +kit --skill path/to/skill.md "prompt" + +# Load multiple skill files or directories (flag is repeatable) +kit --skill ./skill1.md --skill ./skill2.md "prompt" + +# Load all skills from a custom directory instead of the default locations +kit --skills-dir /path/to/skills "prompt" + +# Disable all skill loading (auto-discovery and explicit) +kit --no-skills "prompt" +``` + +Skills are auto-discovered from `~/.config/kit/skills/`, `.kit/skills/`, and `.agents/skills/` by default. Use `--skills-dir` to override the project-local search root, or `--skill` to load files explicitly (which disables auto-discovery). `--no-skills` suppresses all skill loading regardless of other flags. + ## Interactive slash commands These commands are available inside the Kit TUI during an interactive session: diff --git a/www/pages/cli/flags.md b/www/pages/cli/flags.md index f85daa64..17f46590 100644 --- a/www/pages/cli/flags.md +++ b/www/pages/cli/flags.md @@ -48,6 +48,14 @@ These flags control Kit's behavior. When a prompt is passed as a positional argu | `--prompt-template` | — | — | Load a specific prompt template by name | | `--no-prompt-templates` | — | `false` | Disable prompt template loading | +## Skills + +| Flag | Short | Default | Description | +|------|-------|---------|-------------| +| `--skill` | — | — | Load skill file or directory (repeatable) | +| `--skills-dir` | — | — | Override the project-local skills directory for auto-discovery | +| `--no-skills` | — | `false` | Disable skill loading (auto-discovery and explicit) | + ## Generation parameters | Flag | Short | Default | Description | From e1c58ac7079bfef66f0adb6c9e1b22ea46c5d926 Mon Sep 17 00:00:00 2001 From: Sai Karthik Date: Thu, 11 Jun 2026 19:40:18 +0530 Subject: [PATCH 3/5] feat: add config file support for skills options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skills could previously only be controlled via CLI flags or SDK Options fields. This commit wires all three skills settings into viper so they can also be set in .kit.yml / .kit.yaml / .kit.json and via KIT_* environment variables — matching the pattern used by no-extensions, no-core-tools, and prompt-template. cmd/root.go: - Bind --no-skills, --skill, and --skills-dir flags to viper keys (no-skills, skill, skills-dir) so config file values flow through. pkg/kit/kit.go: - At skill-load time, merge opts fields with viper values: - noSkills = opts.NoSkills || v.GetBool("no-skills") - skillPaths: opts.Skills if non-empty, else v.GetStringSlice("skill") - skillsDir: opts.SkillsDir if non-empty, else v.GetString("skills-dir") - Build a shallow-copied mergedOpts so loadSkills() picks up the resolved values without mutating the original Options struct. docs: - README.md: add skills keys to the Basic Configuration YAML example - www/pages/configuration.md: add no-skills, skill, skills-dir rows to the All configuration keys table Config file example (.kit.yml): no-skills: false skill: - /path/to/skill.md skills-dir: /path/to/skills/ Co-authored-by: Claude --- README.md | 6 ++++++ cmd/root.go | 3 +++ pkg/kit/kit.go | 20 ++++++++++++++++++-- www/pages/configuration.md | 3 +++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 19689385..84c7f186 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,12 @@ temperature: 0.7 stream: true thinking-level: off # off, none, minimal, low, medium, high no-core-tools: false # set to true to disable all built-in core tools + +# Skills — all three keys are optional +no-skills: false # set to true to disable all skill loading +skill: # explicit skill files/dirs (disables auto-discovery) + - /path/to/skill.md +skills-dir: "" # override project-local directory for auto-discovery ``` All of the above keys can also be set programmatically via the SDK diff --git a/cmd/root.go b/cmd/root.go index cd4be842..03566fef 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -347,6 +347,9 @@ func init() { _ = viper.BindPFlag("extension", rootCmd.PersistentFlags().Lookup("extension")) _ = viper.BindPFlag("prompt-template", rootCmd.PersistentFlags().Lookup("prompt-template")) _ = viper.BindPFlag("no-prompt-templates", rootCmd.PersistentFlags().Lookup("no-prompt-templates")) + _ = viper.BindPFlag("no-skills", rootCmd.PersistentFlags().Lookup("no-skills")) + _ = viper.BindPFlag("skill", rootCmd.PersistentFlags().Lookup("skill")) + _ = viper.BindPFlag("skills-dir", rootCmd.PersistentFlags().Lookup("skills-dir")) // Defaults are already set in flag definitions, no need to duplicate in viper diff --git a/pkg/kit/kit.go b/pkg/kit/kit.go index f46b8c2a..aff7e059 100644 --- a/pkg/kit/kit.go +++ b/pkg/kit/kit.go @@ -1330,9 +1330,25 @@ func New(ctx context.Context, opts *Options) (*Kit, error) { } // Load skills — either from explicit paths or via auto-discovery. - if !opts.NoSkills { + // Merge viper config with opts: CLI flag / config file values are + // already bound to viper by cmd/root.go, so v.GetBool("no-skills"), + // v.GetStringSlice("skill"), and v.GetString("skills-dir") capture + // both --flag and .kit.yml keys transparently. + noSkills := opts.NoSkills || v.GetBool("no-skills") + skillPaths := opts.Skills + if len(skillPaths) == 0 { + skillPaths = v.GetStringSlice("skill") + } + skillsDir := opts.SkillsDir + if skillsDir == "" { + skillsDir = v.GetString("skills-dir") + } + if !noSkills { + mergedOpts := *opts + mergedOpts.Skills = skillPaths + mergedOpts.SkillsDir = skillsDir var err error - loadedSkills, err = loadSkills(opts) + loadedSkills, err = loadSkills(&mergedOpts) if err != nil { return fmt.Errorf("failed to load skills: %w", err) } diff --git a/www/pages/configuration.md b/www/pages/configuration.md index 79fa852e..7f7f9fe7 100644 --- a/www/pages/configuration.md +++ b/www/pages/configuration.md @@ -47,6 +47,9 @@ stream: true | `theme` | object or string | — | UI theme ([inline overrides or file path](/themes)) | | `prompt-templates` | bool | `true` | Enable prompt template loading | | `prompt-template` | string | — | Specific template to load by name | +| `no-skills` | bool | `false` | Disable skill loading (auto-discovery and explicit) | +| `skill` | list | — | Explicit skill files or directories to load (disables auto-discovery) | +| `skills-dir` | string | — | Override the project-local directory used for skill auto-discovery | ## Environment variables From 80c601a9ac5e9aa26ae40d068241b537595e7d33 Mon Sep 17 00:00:00 2001 From: Sai Karthik Date: Thu, 11 Jun 2026 19:45:10 +0530 Subject: [PATCH 4/5] config: add skills keys to default .kit.yml template Add no-skills, skill, and skills-dir as commented-out examples in the default config file generated by EnsureConfigExists(), alongside the existing application settings block. Co-authored-by: Claude --- internal/config/config.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/config/config.go b/internal/config/config.go index 33428d41..b6312fb2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -493,6 +493,12 @@ mcpServers: # maxTokens: 16384 # systemPrompt: "You are a deep reasoning assistant." # or a file path +# Skills configuration (all optional) +# no-skills: false # Set to true to disable all skill loading +# skill: # Explicit skill files/dirs (disables auto-discovery) +# - "/path/to/skill.md" +# skills-dir: "/path/to/skills" # Override project-local directory for auto-discovery + # API Configuration (can also use environment variables) # provider-api-key: "your-api-key" # API key for OpenAI, Anthropic, or Google # provider-url: "https://api.openai.com/v1" # Base URL for OpenAI, Anthropic, or Ollama From 74fc6ff6282f88d41a974dbed5d17ab3d8025958 Mon Sep 17 00:00:00 2001 From: Sai Karthik Date: Thu, 11 Jun 2026 19:48:46 +0530 Subject: [PATCH 5/5] test: add test coverage for skills CLI flags and config keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four test locations updated: pkg/kit/export_test.go: - Add ConfigStringSliceForTest() helper to expose v.GetStringSlice() from the Kit's isolated viper store, needed to assert skill list values. pkg/kit/kit_test.go (TestNewWithSkillsOptions): - NoSkills=true: GetSkills() returns empty slice - SkillsDir=: kit.New() succeeds with zero skills - Skills=[file]: single explicit skill file is loaded and name parsed correctly pkg/kit/viper_isolation_test.go: - TestSkillsViperKeys: no-API-key struct-level checks for NoSkills, Skills, and SkillsDir fields on Options - TestSkillsConfigFileKeys: full kit.New() round-trips via a written .kit.yml for each of the three config keys: no-skills: true → GetSkills() returns empty skill: [path] → named skill loaded from config file path skills-dir: dir → custom discovery root accepted without error internal/config/config_test.go (TestEnsureConfigExists): - Assert generated ~/.kit.yml template contains '# Skills configuration', 'no-skills:', and 'skills-dir:' comment blocks. Co-authored-by: Claude --- internal/config/config_test.go | 3 + pkg/kit/export_test.go | 6 ++ pkg/kit/kit_test.go | 75 +++++++++++++++++++ pkg/kit/viper_isolation_test.go | 125 ++++++++++++++++++++++++++++++++ 4 files changed, 209 insertions(+) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 095acfb6..8aef1f3d 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -205,6 +205,9 @@ func TestEnsureConfigExists(t *testing.T) { "type: \"local\"", "type: \"remote\"", "Core tools", + "# Skills configuration", + "no-skills:", + "skills-dir:", } for _, expected := range expectedSections { diff --git a/pkg/kit/export_test.go b/pkg/kit/export_test.go index f42f5514..ade7221a 100644 --- a/pkg/kit/export_test.go +++ b/pkg/kit/export_test.go @@ -20,3 +20,9 @@ func (m *Kit) ConfigFloatForTest(key string) float64 { return m.v.GetFloat64(key // ConfigBoolForTest returns the bool value of key from this Kit's isolated // configuration store. func (m *Kit) ConfigBoolForTest(key string) bool { return m.v.GetBool(key) } + +// ConfigStringSliceForTest returns the string slice value of key from this +// Kit's isolated configuration store. +func (m *Kit) ConfigStringSliceForTest(key string) []string { + return m.v.GetStringSlice(key) +} diff --git a/pkg/kit/kit_test.go b/pkg/kit/kit_test.go index 3bd9d957..81256549 100644 --- a/pkg/kit/kit_test.go +++ b/pkg/kit/kit_test.go @@ -365,6 +365,81 @@ func TestNewSystemPromptFilePath(t *testing.T) { } } +// TestNewWithSkillsOptions verifies that the three skills-related Options +// fields (NoSkills, Skills, SkillsDir) are wired correctly into kit.New(). +func TestNewWithSkillsOptions(t *testing.T) { + if os.Getenv("ANTHROPIC_API_KEY") == "" { + t.Skip("Skipping test: ANTHROPIC_API_KEY not set") + } + + ctx := context.Background() + + t.Run("NoSkills disables skill loading", func(t *testing.T) { + host, err := kit.New(ctx, &kit.Options{ + Model: "anthropic/claude-sonnet-4-5-20250929", + Quiet: true, + NoSession: true, + NoSkills: true, + }) + if err != nil { + t.Fatalf("kit.New failed: %v", err) + } + defer func() { _ = host.Close() }() + + if got := host.GetSkills(); len(got) != 0 { + t.Errorf("NoSkills=true: expected 0 skills, got %d", len(got)) + } + }) + + t.Run("SkillsDir propagates", func(t *testing.T) { + // Use a non-existent dir — no skills will load but the option must be + // accepted without error and result in zero skills. + dir := t.TempDir() + host, err := kit.New(ctx, &kit.Options{ + Model: "anthropic/claude-sonnet-4-5-20250929", + Quiet: true, + NoSession: true, + SkillsDir: dir, + }) + if err != nil { + t.Fatalf("kit.New failed: %v", err) + } + defer func() { _ = host.Close() }() + + // Empty dir → no skills; the important thing is no error. + _ = host.GetSkills() + }) + + t.Run("explicit Skills paths load correctly", func(t *testing.T) { + // Write a minimal skill file to a temp dir. + dir := t.TempDir() + skillFile := dir + "/my-skill.md" + content := "---\nname: test-skill\ndescription: A test skill\n---\nDo the thing.\n" + if err := os.WriteFile(skillFile, []byte(content), 0o644); err != nil { + t.Fatalf("failed to write skill file: %v", err) + } + + host, err := kit.New(ctx, &kit.Options{ + Model: "anthropic/claude-sonnet-4-5-20250929", + Quiet: true, + NoSession: true, + Skills: []string{skillFile}, + }) + if err != nil { + t.Fatalf("kit.New failed: %v", err) + } + defer func() { _ = host.Close() }() + + skills := host.GetSkills() + if len(skills) != 1 { + t.Fatalf("expected 1 skill, got %d", len(skills)) + } + if skills[0].Name != "test-skill" { + t.Errorf("skill name = %q; want %q", skills[0].Name, "test-skill") + } + }) +} + // TestNewSystemPromptInline confirms that inline system-prompt strings still // flow through unchanged after the file-path resolution change. func TestNewSystemPromptInline(t *testing.T) { diff --git a/pkg/kit/viper_isolation_test.go b/pkg/kit/viper_isolation_test.go index 3cca7063..c0b4ba2a 100644 --- a/pkg/kit/viper_isolation_test.go +++ b/pkg/kit/viper_isolation_test.go @@ -205,6 +205,131 @@ func TestNewZeroOptionsKeepsStreamingDefault(t *testing.T) { } } +// TestSkillsViperKeys verifies that the three skills config keys (no-skills, +// skill, skills-dir) flow through viper when set via a config file, matching +// the pattern used by no-extensions and no-core-tools. This test does not +// require an API key because it only exercises Options struct plumbing. +func TestSkillsViperKeys(t *testing.T) { + t.Run("NoSkills option disables skill loading", func(t *testing.T) { + o := &kit.Options{} + o.NoSkills = true + if !o.NoSkills { + t.Error("Options.NoSkills = true not reflected on struct") + } + }) + + t.Run("Skills paths set on Options", func(t *testing.T) { + o := &kit.Options{ + Skills: []string{"/a/skill.md", "/b/skill.md"}, + } + if len(o.Skills) != 2 { + t.Errorf("Options.Skills: got %d paths, want 2", len(o.Skills)) + } + if o.Skills[0] != "/a/skill.md" { + t.Errorf("Options.Skills[0] = %q; want %q", o.Skills[0], "/a/skill.md") + } + }) + + t.Run("SkillsDir set on Options", func(t *testing.T) { + o := &kit.Options{ + SkillsDir: "/custom/skills", + } + if o.SkillsDir != "/custom/skills" { + t.Errorf("Options.SkillsDir = %q; want %q", o.SkillsDir, "/custom/skills") + } + }) +} + +// TestSkillsConfigFileKeys verifies that no-skills, skill, and skills-dir +// config file keys are read via viper and applied correctly. Requires an API +// key because kit.New() is called to exercise the full config-load path. +func TestSkillsConfigFileKeys(t *testing.T) { + if os.Getenv("ANTHROPIC_API_KEY") == "" { + t.Skip("Skipping test: ANTHROPIC_API_KEY not set") + } + + ctx := context.Background() + + t.Run("no-skills config key disables skill loading", func(t *testing.T) { + // Write a config file with no-skills: true. + cfgFile := t.TempDir() + "/.kit.yml" + if err := os.WriteFile(cfgFile, []byte("no-skills: true\n"), 0o644); err != nil { + t.Fatalf("failed to write config: %v", err) + } + + host, err := kit.New(ctx, &kit.Options{ + Model: "anthropic/claude-sonnet-4-5-20250929", + Quiet: true, + NoSession: true, + ConfigFile: cfgFile, + }) + if err != nil { + t.Fatalf("kit.New failed: %v", err) + } + defer func() { _ = host.Close() }() + + if got := host.GetSkills(); len(got) != 0 { + t.Errorf("no-skills:true in config: expected 0 skills, got %d", len(got)) + } + }) + + t.Run("skill config key loads explicit skill files", func(t *testing.T) { + dir := t.TempDir() + skillFile := dir + "/cfg-skill.md" + if err := os.WriteFile(skillFile, []byte("---\nname: cfg-skill\ndescription: from config\n---\nContent.\n"), 0o644); err != nil { + t.Fatalf("failed to write skill file: %v", err) + } + + cfgContent := "skill:\n - " + skillFile + "\n" + cfgFile := dir + "/.kit.yml" + if err := os.WriteFile(cfgFile, []byte(cfgContent), 0o644); err != nil { + t.Fatalf("failed to write config: %v", err) + } + + host, err := kit.New(ctx, &kit.Options{ + Model: "anthropic/claude-sonnet-4-5-20250929", + Quiet: true, + NoSession: true, + ConfigFile: cfgFile, + }) + if err != nil { + t.Fatalf("kit.New failed: %v", err) + } + defer func() { _ = host.Close() }() + + skills := host.GetSkills() + if len(skills) != 1 { + t.Fatalf("expected 1 skill from config, got %d", len(skills)) + } + if skills[0].Name != "cfg-skill" { + t.Errorf("skill name = %q; want %q", skills[0].Name, "cfg-skill") + } + }) + + t.Run("skills-dir config key overrides auto-discovery root", func(t *testing.T) { + dir := t.TempDir() + cfgContent := "skills-dir: " + dir + "\n" + cfgFile := dir + "/.kit.yml" + if err := os.WriteFile(cfgFile, []byte(cfgContent), 0o644); err != nil { + t.Fatalf("failed to write config: %v", err) + } + + host, err := kit.New(ctx, &kit.Options{ + Model: "anthropic/claude-sonnet-4-5-20250929", + Quiet: true, + NoSession: true, + ConfigFile: cfgFile, + }) + if err != nil { + t.Fatalf("kit.New failed: %v", err) + } + defer func() { _ = host.Close() }() + + // Empty dir → 0 skills; the key point is no error during init. + _ = host.GetSkills() + }) +} + // TestNewStreamingExplicitOptOut verifies that a raw Options can still disable // streaming by setting Streaming to a pointer to false. func TestNewStreamingExplicitOptOut(t *testing.T) {