Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/disabled-skills-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Add `disabled_skills` config to fully hide selected skill names from Kimi (model listing, Skill tool, slash menu, and activation). Set `disabled_skills = ["name"]` in `config.toml`, then run `/reload`.
1 change: 1 addition & 0 deletions apps/kimi-web/src/api/daemon/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1275,6 +1275,7 @@ export class DaemonKimiWebApi implements KimiWebApi {
services: 'services',
mergeAllAvailableSkills: 'merge_all_available_skills',
extraSkillDirs: 'extra_skill_dirs',
disabledSkills: 'disabled_skills',
loopControl: 'loop_control',
background: 'background',
experimental: 'experimental',
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-web/src/api/daemon/mappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,7 @@ export function toAppConfig(wire: WireConfig): AppConfig {
services: wire.services,
mergeAllAvailableSkills: wire.merge_all_available_skills,
extraSkillDirs: wire.extra_skill_dirs,
disabledSkills: wire.disabled_skills,
loopControl: wire.loop_control,
background: wire.background,
experimental: wire.experimental,
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-web/src/api/daemon/wire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,7 @@ export interface WireConfig {
services?: unknown;
merge_all_available_skills?: boolean;
extra_skill_dirs?: string[];
disabled_skills?: string[];
loop_control?: unknown;
background?: unknown;
experimental?: Record<string, boolean>;
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-web/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,7 @@ export interface AppConfig {
services?: unknown;
mergeAllAvailableSkills?: boolean;
extraSkillDirs?: string[];
disabledSkills?: string[];
loopControl?: unknown;
background?: unknown;
experimental?: Record<string, boolean>;
Expand Down
17 changes: 17 additions & 0 deletions docs/en/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ Fields in the config file fall into two categories: **top-level scalars** that d
| `default_plan_mode` | `boolean` | `false` | Whether new sessions start in Plan mode (produce a plan before executing) by default |
| `merge_all_available_skills` | `boolean` | `true` | Whether to merge Agent Skills from all available directories |
| `extra_skill_dirs` | `array<string>` | — | Extra skill search directories, layered on top of the default directories |
| `disabled_skills` | `array<string>` | `[]` | Skill names to fully disable in Kimi (model listing, Skill tool, slash menu, and user activation). Case-insensitive. Files stay on disk. Does not block `Bash` or other tools that reimplement a skill's workflow — pair with [`permission`](#permission) deny rules when needed. See [Agent Skills](../customization/skills.md#skill-locations) |
| `telemetry` | `boolean` | `true` | Whether anonymous telemetry is enabled; disabled only when explicitly set to `false` |
| `providers` | `table` | `{}` | API provider table → [`providers`](#providers) |
| `models` | `table` | — | Model alias table → [`models`](#models) |
Expand Down Expand Up @@ -292,6 +293,8 @@ api_key = "sk-xxx"

Built-in tool names are listed in [Built-in tools](../reference/tools.md). Most built-in tools that accept rule arguments define their own matching subject, such as `Bash(command-pattern)` or `Read(path-pattern)`. `AgentSwarm`, MCP tools, and custom tools can only be matched by tool name — argument patterns are not supported for them.

**Permission mode vs deny:** `default_permission_mode` (`manual` / `yolo` / `auto`) only changes what happens when no deny rule matches. A `decision = "deny"` rule always blocks the matching tool call, including in YOLO mode.

```toml
[[permission.rules]]
decision = "allow"
Expand All @@ -310,6 +313,20 @@ decision = "ask"
pattern = "Bash"
```

To hide a skill from the model **and** block shell helpers that reimplement it (for example after `disabled_skills`):

```toml
disabled_skills = ["grok-delegation", "pi-delegation"]

[[permission.rules]]
decision = "deny"
pattern = "Bash(*grok-delegate*)"

[[permission.rules]]
decision = "deny"
pattern = "Bash(*pi-delegate*)"
```

::: tip
MCP server declarations are configured in `~/.kimi-code/mcp.json` or the project-local `.kimi-code/mcp.json`, not in `config.toml`. The interactive configuration entry point is `/mcp-config`; see [Model Context Protocol](../customization/mcp.md).
:::
Expand Down
26 changes: 26 additions & 0 deletions docs/en/customization/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,32 @@ The Kimi-specific user Skill directory moves with `KIMI_CODE_HOME`, so isolated
extra_skill_dirs = ["~/team-skills", ".agents/team-skills"]
```

**Disabling skills by name**: Use top-level `disabled_skills` when a Skill directory is shared with other tools (for example `~/.agents/skills/`) but you do not want selected Skills to appear in Kimi at all:

```toml
disabled_skills = ["grok-delegation", "pi-delegation"]
```

Disabled names are case-insensitive. Matching Skills are removed from the model listing, rejected by the `Skill` tool, hidden from the slash menu, and blocked from user activation. Files remain on disk for other tools. After editing `config.toml`, run `/reload` or start a new session.

This is stronger than frontmatter `disableModelInvocation: true` (which only blocks automatic model invocation while still allowing slash use) and stronger than a permission deny rule for `Skill(...)` (which can block the tool call but still leaves the Skill in the model listing).

`disabled_skills` only covers the Skill surface. The agent may still reimplement a skill's workflow with other tools — for example running a helper binary via `Bash` because a handoff document or earlier conversation showed that command. To block those shell paths as well, add deny rules (deny still applies in YOLO mode):

```toml
disabled_skills = ["grok-delegation", "pi-delegation"]

[[permission.rules]]
decision = "deny"
pattern = "Bash(*grok-delegate*)"

[[permission.rules]]
decision = "deny"
pattern = "Bash(*pi-delegate*)"
```

See [Permission rules](../configuration/config-files.md#permission) for the full rule format.

**Built-in Skills** are distributed with the CLI and have the lowest priority. They provide out-of-the-box workflows for common tasks — for example, configuring MCP servers, customizing the TUI theme, and editing config files. See [Built-in skill commands](../reference/slash-commands.md#built-in-skill-commands) for the full list.

## Invoking a Skill
Expand Down
17 changes: 17 additions & 0 deletions docs/zh/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ timeout = 5
| `default_plan_mode` | `boolean` | `false` | 新会话是否默认以 Plan 模式(先出计划再执行)启动 |
| `merge_all_available_skills` | `boolean` | `true` | 是否合并所有目录中的 Agent Skills |
| `extra_skill_dirs` | `array<string>` | — | 额外 Skill 搜索目录,叠加到默认目录之上 |
| `disabled_skills` | `array<string>` | `[]` | 在 Kimi 中完全禁用的 Skill 名称(模型列表、Skill 工具、斜杠菜单与用户激活)。大小写不敏感。磁盘上的文件保留。不拦截用 `Bash` 等工具「复现」Skill 工作流的行为——需要时配合 [`permission`](#permission) deny 规则。详见 [Agent Skills](../customization/skills.md#skill-存放位置) |
| `telemetry` | `boolean` | `true` | 是否启用匿名遥测;显式设为 `false` 时关闭 |
| `providers` | `table` | `{}` | API 供应商表 → [`providers`](#providers) |
| `models` | `table` | — | 模型别名表 → [`models`](#models) |
Expand Down Expand Up @@ -292,6 +293,8 @@ api_key = "sk-xxx"

内置工具名见[内置工具](../reference/tools.md)。大多数支持规则参数的内置工具会定义自己的匹配对象,例如 `Bash(command-pattern)` 或 `Read(path-pattern)`。`AgentSwarm`、MCP 工具和自定义工具只能按工具名匹配,不支持参数模式。

**权限模式与 deny:** `default_permission_mode`(`manual` / `yolo` / `auto`)只影响「没有 deny 命中时」的行为。`decision = "deny"` 的规则始终拦截匹配的工具调用,**包括 YOLO 模式**。

```toml
[[permission.rules]]
decision = "allow"
Expand All @@ -310,6 +313,20 @@ decision = "ask"
pattern = "Bash"
```

若要在隐藏 Skill 的同时拦住「用 shell 复现其工作流」的辅助命令(例如配合 `disabled_skills`):

```toml
disabled_skills = ["grok-delegation", "pi-delegation"]

[[permission.rules]]
decision = "deny"
pattern = "Bash(*grok-delegate*)"

[[permission.rules]]
decision = "deny"
pattern = "Bash(*pi-delegate*)"
```

::: tip
MCP server 的声明配置写在 `~/.kimi-code/mcp.json` 或项目内 `.kimi-code/mcp.json` 中,不在 `config.toml` 里。交互式配置入口是 `/mcp-config`,详见 [Model Context Protocol](../customization/mcp.md)。
:::
Expand Down
26 changes: 26 additions & 0 deletions docs/zh/customization/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,32 @@ Kimi 专属用户级 Skill 目录会随 `KIMI_CODE_HOME` 移动,因此隔离
extra_skill_dirs = ["~/team-skills", ".agents/team-skills"]
```

**按名称禁用 Skill**:当 Skill 目录与其他工具共享(例如 `~/.agents/skills/`),但又不希望某些 Skill 出现在 Kimi 中时,使用顶层 `disabled_skills`:

```toml
disabled_skills = ["grok-delegation", "pi-delegation"]
```

名称匹配不区分大小写。被禁用的 Skill 会从模型列表中移除,被 `Skill` 工具拒绝,不出现在斜杠菜单中,也无法由用户激活。磁盘上的文件会保留,供其他工具使用。修改 `config.toml` 后执行 `/reload` 或开启新会话。

这比 frontmatter 的 `disableModelInvocation: true` 更强(后者只阻止模型自动调用,仍允许斜杠调用),也比针对 `Skill(...)` 的 permission deny 规则更强(后者可能拦截工具调用,但 Skill 仍会出现在模型列表中)。

`disabled_skills` 只覆盖 Skill 表面。Agent 仍可能用其他工具「复现」Skill 的工作流——例如根据 handoff 文档或先前对话,用 `Bash` 直接跑辅助二进制。若要一并拦住这类 shell 路径,再加 deny 规则(YOLO 模式下 deny 仍然生效):

```toml
disabled_skills = ["grok-delegation", "pi-delegation"]

[[permission.rules]]
decision = "deny"
pattern = "Bash(*grok-delegate*)"

[[permission.rules]]
decision = "deny"
pattern = "Bash(*pi-delegate*)"
```

完整规则格式见 [Permission 规则](../configuration/config-files.md#permission)。

**内置 Skills** 随 CLI 一起分发,优先级最低。它们为常见任务提供开箱即用的工作流,例如配置 MCP server、定制 TUI 主题和编辑配置文件。完整列表详见[内置 Skill 命令](../reference/slash-commands.md#内置-skill-命令)。

## 调用 Skill
Expand Down
6 changes: 6 additions & 0 deletions packages/agent-core-v2/src/agent/skill/skillService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ export class AgentSkillService extends Disposable implements IAgentSkillService
if (skill === undefined) {
throw new Error2(ErrorCodes.SKILL_NOT_FOUND, `Skill "${input.name}" was not found`);
}
if (this.skillCatalog.catalog.isSkillDisabled(input.name)) {
throw new Error2(
ErrorCodes.SKILL_DISABLED,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Map disabled skill activation to a client error

When a disabled skill is activated through POST /sessions/{id}/skills/{name}:activate, this new ErrorCodes.SKILL_DISABLED path reaches packages/kap-server/src/routes/skills.ts's sendMappedError, which only handles SKILL_NOT_FOUND, SKILL_NAME_EMPTY, and SKILL_TYPE_UNSUPPORTED; the error is then rethrown to the global handler as 50001. A direct REST call or stale slash-menu entry for a disabled skill should return a deterministic client error instead of an internal error, so add this code to the route/error-code mapping.

Useful? React with 👍 / 👎.

`Skill "${skill.name}" is disabled in configuration (disabled_skills).`,
);
}
if (!isUserActivatableSkillType(skill.metadata.type)) {
throw new Error2(
ErrorCodes.SKILL_TYPE_UNSUPPORTED,
Expand Down
5 changes: 5 additions & 0 deletions packages/agent-core-v2/src/agent/skill/tools/skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ export async function executeModelSkill(
if (skill === undefined) {
return errorResult(`Skill "${args.skill}" not found in the current skill listing.`);
}
if (catalog.catalog.isSkillDisabled(args.skill)) {
return errorResult(
`Skill "${skill.name}" is disabled in configuration (disabled_skills).`,
);
}
if (skill.metadata.disableModelInvocation === true) {
return errorResult(
`Skill "${args.skill}" can only be triggered by the user (model invocation is disabled).`,
Expand Down
15 changes: 12 additions & 3 deletions packages/agent-core-v2/src/app/skillCatalog/configSection.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/**
* `skillCatalog` domain (L3) — skill config sections.
*
* Registers the v1-compatible top-level config domains `extraSkillDirs` and
* `mergeAllAvailableSkills`. Values stay camelCase in memory; TOML uses the
* snake_case keys `extra_skill_dirs` and `merge_all_available_skills`.
* Registers the v1-compatible top-level config domains `extraSkillDirs`,
* `mergeAllAvailableSkills`, and `disabledSkills`. Values stay camelCase in
* memory; TOML uses the snake_case keys `extra_skill_dirs`,
* `merge_all_available_skills`, and `disabled_skills`.
*/

import { z } from 'zod';
Expand All @@ -25,3 +26,11 @@ export type MergeAllAvailableSkillsConfig = z.infer<typeof MergeAllAvailableSkil
registerConfigSection(MERGE_ALL_AVAILABLE_SKILLS_SECTION, MergeAllAvailableSkillsConfigSchema, {
defaultValue: true,
});

export const DISABLED_SKILLS_SECTION = 'disabledSkills';
export const DisabledSkillsConfigSchema = z.array(z.string()).optional();
export type DisabledSkillsConfig = z.infer<typeof DisabledSkillsConfigSchema>;

registerConfigSection(DISABLED_SKILLS_SECTION, DisabledSkillsConfigSchema, {
defaultValue: [],
});
1 change: 1 addition & 0 deletions packages/agent-core-v2/src/app/skillCatalog/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const SkillErrors = {
SKILL_NOT_FOUND: 'skill.not_found',
SKILL_TYPE_UNSUPPORTED: 'skill.type_unsupported',
SKILL_NAME_EMPTY: 'skill.name_empty',
SKILL_DISABLED: 'skill.disabled',
},
} as const satisfies ErrorDomain;

Expand Down
25 changes: 23 additions & 2 deletions packages/agent-core-v2/src/app/skillCatalog/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ import type {
SkillSource,
SkippedSkill,
} from './types';
import { isInlineSkillType, normalizeSkillName } from './types';
import {
createDisabledSkillNameSet,
isDisabledSkillName,
isInlineSkillType,
normalizeSkillName,
} from './types';

const LISTING_DESC_MAX = 250;

Expand All @@ -32,11 +37,21 @@ export class SkillNotFoundError extends Error {
}
}

export interface InMemorySkillCatalogOptions {
/** Skill names from config `disabled_skills` (case-insensitive). */
readonly disabledSkills?: readonly string[];
}

export class InMemorySkillCatalog implements SkillCatalog {
private readonly byName = new Map<string, SkillDefinition>();
private readonly byPluginAndName = new Map<string, SkillDefinition>();
private readonly roots: string[] = [];
private readonly skipped: SkippedSkill[] = [];
private readonly disabledSkills: ReadonlySet<string>;

constructor(options: InMemorySkillCatalogOptions = {}) {
this.disabledSkills = createDisabledSkillNameSet(options.disabledSkills);
}

registerBuiltinSkill(skill: SkillDefinition): void {
this.register(skill.source === 'builtin' ? skill : { ...skill, source: 'builtin' });
Expand Down Expand Up @@ -90,8 +105,14 @@ export class InMemorySkillCatalog implements SkillCatalog {
);
}

isSkillDisabled(name: string): boolean {
return isDisabledSkillName(name, this.disabledSkills);
}

listSkills(): readonly SkillDefinition[] {
return [...this.byName.values()].toSorted((a, b) => a.name.localeCompare(b.name));
return [...this.byName.values()]
.filter((skill) => !this.isSkillDisabled(skill.name))
.toSorted((a, b) => a.name.localeCompare(b.name));
}

listInvocableSkills(): readonly SkillDefinition[] {
Expand Down
23 changes: 23 additions & 0 deletions packages/agent-core-v2/src/app/skillCatalog/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,35 @@ export interface SkillCatalog {
getSkillRoots(): readonly string[];
getSkippedByPolicy(): readonly SkippedSkill[];
getModelSkillListing(): string;
/** True when the skill name is listed in config `disabled_skills`. */
isSkillDisabled(name: string): boolean;
}

export function normalizeSkillName(name: string): string {
return name.toLowerCase();
}

/** Build a case-insensitive set of skill names from config `disabled_skills`. */
export function createDisabledSkillNameSet(
names: readonly string[] | undefined,
): ReadonlySet<string> {
const set = new Set<string>();
if (names === undefined) return set;
for (const name of names) {
const normalized = normalizeSkillName(name.trim());
if (normalized.length > 0) set.add(normalized);
}
return set;
}

export function isDisabledSkillName(name: string, disabled: ReadonlySet<string>): boolean {
return disabled.has(normalizeSkillName(name));
}

export function disabledSkillConfigMessage(skillName: string): string {
return `Skill "${skillName}" is disabled in configuration (disabled_skills).`;
}

export function isInlineSkillType(type: string | undefined): boolean {
return type === undefined || type === 'prompt' || type === 'inline';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import { Disposable } from '#/_base/di/lifecycle';
import { InstantiationType } from '#/_base/di/extensions';
import { Emitter, type Event } from '#/_base/event';
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
import { IConfigService } from '#/app/config/config';
import { IBuiltinSkillSource } from '#/app/skillCatalog/builtinSkillSource';
import {
DISABLED_SKILLS_SECTION,
type DisabledSkillsConfig,
} from '#/app/skillCatalog/configSection';
import { InMemorySkillCatalog } from '#/app/skillCatalog/registry';
import type { ISkillSource, SkillContribution } from '#/app/skillCatalog/skillSource';
import type { SkillCatalog } from '#/app/skillCatalog/types';
Expand Down Expand Up @@ -47,12 +52,21 @@ export class SessionSkillCatalogService
@IExtraFileSkillSource extra: IExtraFileSkillSource,
@IWorkspaceFileSkillSource workspace: IWorkspaceFileSkillSource,
@IPluginSkillSource plugin: IPluginSkillSource,
@IConfigService private readonly config: IConfigService,
) {
super();
this.sources = [builtin, user, explicit, extra, workspace, plugin].toSorted((a, b) => a.priority - b.priority);
for (const s of this.sources) {
if (s.onDidChange) this._register(s.onDidChange(() => { void this.reloadSource(s.id); }));
}
this._register(
this.config.onDidSectionChange((event) => {
if (event.domain === DISABLED_SKILLS_SECTION) {
this.remerge();
this.onDidChangeEmitter.fire('disabledSkills');
}
}),
);
this.ready = this.loadAll();
}

Expand Down Expand Up @@ -115,7 +129,9 @@ export class SessionSkillCatalogService
}

private remerge(): void {
const m = new InMemorySkillCatalog();
const disabledSkills =
this.config.get<DisabledSkillsConfig>(DISABLED_SKILLS_SECTION) ?? [];
const m = new InMemorySkillCatalog({ disabledSkills });
Comment on lines +132 to +134

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply disabled_skills to workspace skill listings

This wires disabled_skills into the session-scoped catalog, but the no-session workspace listing still composes its own catalog in packages/kap-server/src/routes/skills.ts with new InMemorySkillCatalog() and returns catalog.listSkills(). apps/kimi-web uses GET /workspaces/{id}/skills to pre-warm the onboarding composer's slash menu before a session exists, so disabled skills still appear there until the first session is created; pass the same config into that workspace catalog as well.

Useful? React with 👍 / 👎.

const ordered = [...this.contributions.values()].toSorted((a, b) => a.priority - b.priority);
for (const { c } of ordered) {
for (const skill of c.skills) m.register(skill, { replace: true });
Expand Down
Loading