# 技能与自定义命令

> SKILL.md 从哪些目录被发现、同名时谁先被加载，技能清单怎样以系统提醒进入上下文、Skill 工具怎样读取全文；Markdown 自定义命令的参数替换与 shell 展开及其安全边界，以及内置斜杠命令在 TUI、无头模式与 App 协议里的不同面貌。

- 作者：David（道雾轩）
- 专栏：ZCode 源码解读（https://daiw.org/manual/zcode.md）
- 最后更新：2026-09-21
- 原文：https://daiw.org/manual/zcode/skills-commands
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

ZCode 有两种“写一段 Markdown 就能扩展 Agent”的机制。**技能**是一个目录加一份 `SKILL.md`：模型平时只看到名字和描述，需要时调用 `Skill` 工具把正文读进上下文。**自定义命令**是一份 `.md` 提示词模板：用户输入 `/名字 参数`，模板展开成一条普通提示词交给模型，展开前还能先跑几段 shell。两者再加上 19 个内置斜杠命令，就是用户在输入框里敲 `/` 时能用到的全部东西。

代码分三层：发现与读取在 `apps/zcode-cli/packages/adapters/src/skills/` 与 `apps/zcode-cli/packages/adapters/src/commands/`；装配、命令展开与 shell 执行在 `apps/zcode-cli/packages/bootstrap/src/` 下的 `skills.ts`、`custom-commands.ts`、`custom-command-prompt.ts`、`custom-command-shell-expansion.ts`、`builtin-prompt-command.ts`、`slash-command-surface.ts`；`Skill` 工具与技能清单在 `core/src/tool/handlers/skill.ts` 和 `core/src/context/sections/skills.ts`；终端里的斜杠命令分发在 `cli/src/command-center/`。插件同样能携带技能与命令，插件本身留给[下一篇](https://daiw.org/manual/zcode/plugins)。

## 怎么用

| 操作 | 入口 | 说明 |
| --- | --- | --- |
| 列出技能 | `zcode skills` 或 `zcode skills list` | 每条显示名字、`scope/source`、描述与 `SKILL.md` 路径；`--json` 输出 JSON，`--verbose` 附诊断 |
| 查看一个技能 | `zcode skills inspect <name>` | 另显示 `safeToAutoLoad`、读取字节数与正文 |
| 列出或查看命令 | `zcode commands list`、`zcode commands inspect <name>` | inspect 显示解析出的 frontmatter，`--verbose` 才打印正文 |
| TUI 里列技能 | `/skill` | 与 `zcode skills list` 同一份文本 |
| 点名使用技能 | `/skill <name> [task]` | 改写成一条“先调 Skill 工具”的提示词 |
| 调用自定义命令 | `/<name> [args]` | 展开模板后作为普通提示词提交 |
| 帮助 | `/help [command]` | 内置命令与自定义命令一并列出 |

子命令分派在 `apps/zcode-cli/packages/cli/src/run.ts:546`；`zcode --help` 只写了 `skills list` 与 `commands list`（`apps/zcode-cli/packages/i18n/src/locales/zh-CN.ts:19`、`zh-CN.ts:24`），`inspect` 没有出现在帮助里，但两个子命令都支持它（`apps/zcode-cli/packages/cli/src/skills-command.ts:26`、`apps/zcode-cli/packages/cli/src/commands-command.ts:30`）。

个人技能放 `~/.zcode/skills/<name>/SKILL.md`，项目技能放仓库里的 `.zcode/skills/<name>/SKILL.md`，两处都另认一个 `.agents/skills`；命令对应 `~/.zcode/commands/` 与 `.zcode/commands/`（外加 `.agents/commands`）下的 `.md` 文件。相关配置写在用户配置文件 `~/.zcode/cli/config.json`（`apps/zcode-cli/packages/adapters/src/config/file-config.adapter.ts:61`）或项目的 `.zcode/config.json` 里：

| 键 | 默认 | 作用 |
| --- | --- | --- |
| `features.skill`、`skills.enabled` | 都是 `true` | 两者同时为真才创建技能端口，否则连 `Skill` 工具都不注册 |
| `skills.roots` | `[]` | 额外技能根目录，支持 `~/` 与相对路径 |
| `skills.metadataBudget` | `20000` | 技能清单的字符预算 |
| `skills.includeInstructions` | `true` | 只被解析与合并，运行时没有读取它的地方 |
| `skill.<SKILL.md 绝对路径>.enable` | 无 | 写 `false` 禁用单个技能 |
| `command.<命令文件绝对路径>.enable` | 无 | 写 `false` 禁用单条命令 |

默认值在 `apps/zcode-cli/packages/contracts/src/config/index.ts:313` 与 `contracts/src/config/index.ts:330`。桌面设置页的技能开关写的是复数形式 `skills.<路径>`（`packages/services/src/skills/skillsService.ts:568`），解析配置时两种写法合并成同一张禁用表（`apps/zcode-cli/packages/adapters/src/config/schema.ts:411`），发现阶段直接剔除命中的路径（`apps/zcode-cli/packages/bootstrap/src/skill-command-overrides.ts:5`）。

## 技能从哪些目录来

`resolveDefaultSkillRoots` 按固定顺序排出根目录，每排一个就分到一个递增的 priority，步长 10（`apps/zcode-cli/packages/adapters/src/skills/roots.ts:8`）。核心一段（`skills/roots.ts:34`）：

```ts
  for (const extraRoot of options.extraRoots ?? []) {
    roots.push(
      root(
        resolveConfiguredRoot(extraRoot, resolvedWorkingDirectory),
        "project",
        "zcode",
        nextPriority(),
      ),
    );
  }

  if (includeZcode) {
    roots.push(...skillRootsForBase(home, "user", nextPriority));
  }

  const projectDirectories = await resolveProjectSkillDirectories(resolvedWorkingDirectory);
  for (const directory of projectDirectories) {
    if (includeZcode) {
      roots.push(...skillRootsForBase(directory, "project", nextPriority));
    }
  }

  roots.push(...(options.extraResolvedRoots ?? []));

  return roots;
```

| 顺序 | 根目录 | scope/source |
| --- | --- | --- |
| 1 | `skills.roots` 里的每个目录 | `project/zcode` |
| 2 | `~/.zcode/skills`、`~/.agents/skills` | `user/zcode`、`user/agents` |
| 3 | 从工作目录起逐级向上，每层的 `.zcode/skills`、`.agents/skills`，到含 `.git` 的那一层为止 | `project/zcode`、`project/agents` |
| 4 | 已启用插件的技能目录（由 bootstrap 传入） | 官方插件为 `system/plugin`，其余为 `user/plugin` |

找不到 `.git` 时只看工作目录本身（`skills/roots.ts:61`）。每一层的 `.zcode` 与 `.agents` 是合并而不是回退，注释写明“用户可能同时安装原生 `.zcode` skill 和兼容 `.agents` skill”（`skills/roots.ts:99`）。`.agents` 是注释里所说的“兼容”目录，名字不带任何产品前缀，ZCode 在技能和命令两处都认它；桌面设置页早先把它当成 `.zcode` 的后备，结果同层 `.zcode` 只要有一个技能，`.agents` 整根就从界面上消失，后来改成与 Agent 一致的合并语义（`packages/services/src/skills/skillsService.ts:157`）。插件技能根的 priority 从 1000 起步（`apps/zcode-cli/packages/adapters/src/plugins/index.ts:104`），scope 按插件来源区分（`adapters/src/plugins/index.ts:704`）。

**同名时谁赢**。`discoverSkills` 按路径而不是按名字去重，注释说“同名技能可能来自不同技能生态或版本，不能只按 name 去重；路径才是安装项身份”（`apps/zcode-cli/packages/adapters/src/skills/index.ts:80`），所以同名技能会全部出现在清单里。按名字排序时排序是稳定的（`adapters/src/skills/index.ts:89`），`loadSkill` 取第一个名字匹配的条目（`adapters/src/skills/index.ts:108`），效果是 priority 数值小的根目录胜出。这与多数人的直觉相反：`~/.zcode/skills/foo` 会压过仓库里的 `.zcode/skills/foo`，工作目录这一层压过上层目录，插件技能排在最后，而 `skills.roots` 里配的目录排在所有目录之前。

ZCode 不扫描 Claude Code 的技能目录 `~/.claude/skills` 与 `.claude/skills`（见 [Claude Code 手册的 Skills 篇](https://daiw.org/manual/claude-code/skills)）。想复用那边的技能，只能复制或软链接到 `~/.zcode/skills`：用户级根跟随符号链接，注释明说这是受支持的导入方式（`apps/zcode-cli/packages/adapters/src/skills/scan.ts:23`）。

## 扫描策略

- **只看两层**：根目录自身的 `SKILL.md`，加上一层子目录里的 `SKILL.md`（`scan.ts:43`）。`skills/engineering/foo/SKILL.md` 这种分组布局不会被 Agent 发现。共享策略里的 `MAX_SKILL_SCAN_DEPTH = 8` 只给桌面端的递归扫描用（`packages/shared/src/skill-scan-policy.ts:43`），桌面设置页因此“支持分组目录”（`packages/services/src/skills/skillsService.ts:991`）。结果是分组目录里的技能能在桌面界面上看到，Agent 的 `Skill` 工具却加载不到。
- **跳过的目录**：`node_modules`、`dist`、`build`、`out`、`target`、`vendor`、`coverage`、`.cache`、`.next`、`.turbo`、`.venv`、`__pycache__`（`skill-scan-policy.ts:21`），以及除 `.system` 以外所有以 `.` 开头的目录（`skill-scan-policy.ts:50`）。
- **符号链接**：用户级根跟随链接；插件根在根目录、子目录、`SKILL.md` 三个粒度一律拒绝链接，防止插件借链接读到插件根外的文件（`scan.ts:16`、`apps/zcode-cli/packages/adapters/src/skills/index.ts:141`）。
- **错误**：路径不存在视为常态、返回空；权限错误等其余异常记成 `skill_scan_failed` 诊断，不中断其他根（`adapters/src/skills/index.ts:149`）。
- **大小**：描述超过 1024 个字符的技能直接丢弃（`adapters/src/skills/index.ts:20`、`adapters/src/skills/index.ts:206`）；`Skill` 工具最多读 100000 字节，超出部分截断（`adapters/src/skills/index.ts:28`、`apps/zcode-cli/packages/core/src/tool/handlers/skill.ts:16`）。

## SKILL.md 的格式与 Claude Code 兼容程度

frontmatter 用一个手写的扁平 YAML 解析器读：只认顶层 `键: 值`，缩进行一律跳过，但支持 `>` 与 `|` 两种块标量，免得多行描述只剩一个符号（`adapters/src/skills/index.ts:327`）。规则如下（`adapters/src/skills/index.ts:178`）：

- 没有 frontmatter：名字取目录名，描述可以为空，照样加载。
- 有 frontmatter 却缺 `name` 或 `description`：记 error 级诊断，丢弃该技能。
- `when_to_use` 会拼在描述后面一起进清单（`apps/zcode-cli/packages/core/src/context/sections/skills.ts:61`）。
- `name`、`description`、`when_to_use`、`license`、`metadata` 之外的键不报错，只让 `safeToAutoLoad` 变成 `false`（`adapters/src/skills/index.ts:21`、`adapters/src/skills/index.ts:231`）。这个字段只在 `zcode skills inspect` 里显示，没有任何地方据它拦截加载；同样，`policy.allowImplicitInvocation` 恒为 `true`（`adapters/src/skills/index.ts:234`），也没有消费者。
- 正文里的 `${ZCODE_SKILL_DIR}` 与 `${CLAUDE_SKILL_DIR}` 在加载时替换成技能目录（`tool/handlers/skill.ts:73`）。

所以兼容程度可以概括为：Claude Code 风格的 `SKILL.md` 能原样加载，`CLAUDE_` 前缀的变量名也认，但目录不通用，控制类键不生效。仓库自己的 `.agents/skills` 就是例子：`dep-refs` 写了 `disable-model-invocation: true`（`.agents/skills/dep-refs/SKILL.md:4`），`agent-browser` 写了 `allowed-tools`（`.agents/skills/agent-browser/SKILL.md:4`），在 ZCode 里前者照样可以被模型自动调用，后者也不会放宽任何权限。

## 技能怎样进入上下文

技能在会话初始化上下文时发现一次（`apps/zcode-cli/packages/core/src/runtime/methods/context.ts:65`），之后整场会话用这份快照；输入框要展示技能时也读同一份快照，而不是另扫磁盘（`context.ts:76`）。清单由 `buildSkillsSection` 生成（`core/src/context/sections/skills.ts:39`）：

```ts
function buildSkillsContent(skills: SkillMetadata[], budget: number): string {
  const lines = [
    "The following skills are available for use with the Skill tool:",
    "",
  ];

  const sortedSkills = [...skills].sort((a, b) =>
    skillDisplayName(a).localeCompare(skillDisplayName(b)),
  );
  const skillLines = sortedSkills.map((skill) => formatSkillLine(skill, MAX_DESCRIPTION_CHARS));
  const full = [...lines, ...skillLines].join("\n");
  if (full.length <= budget) {
    return full;
  }

  const namesOnly = sortedSkills.map(
    (skill) => `- ${skillDisplayName(skill)}${bareAliasSuffix(skill)} (file: ${skill.path})`,
  );
  return [...lines, ...namesOnly].join("\n");
}
```

每条是“名字、描述、`(file: 路径)`”，描述截到 250 个字符（`skills.ts:10`）；插件技能显示为 `插件名:技能名`，后缀注明也能用裸名加载（`skills.ts:75`）。整份清单超过预算（默认 20000 个字符）就只列名字和路径，不会逐条丢弃。它**不在系统提示词里**：这一段的注入目标是 `meta_user`，拼成来源为 `skills_listing` 的附件（`apps/zcode-cli/packages/core/src/context/builder.ts:282`），属于请求前缀里的一条系统提醒（`apps/zcode-cli/packages/core/src/system-reminder/source.ts:91`）。当前工具表里没有 `Skill` 时（例如某些工作流子 Agent），整段省略，免得模型以为自己有一个其实不存在的工具（`builder.ts:175`）。提醒与缓存布局的细节见[系统提示词、上下文与提醒](https://daiw.org/manual/zcode/context-builder)。

## Skill 工具

工具输入是 `skill` 加可选的 `args`，旧写法 `name` 仍被接受（`apps/zcode-cli/packages/contracts/src/tools/skill.ts:9`、`contracts/src/tools/skill.ts:21`），但处理函数只取 `skill`，`args` 被解析后就丢掉了（`core/src/tool/handlers/skill.ts:19`）。加载时 `loadSkill` 会重新跑一遍发现，按裸名或 `插件名:技能名` 取第一个匹配，读正文、去掉 frontmatter（`adapters/src/skills/index.ts:95`），然后包装成（`tool/handlers/skill.ts:58`）：

```ts
  return [
    `<skill_content name="${loaded.metadata.name}">`,
    `# Skill: ${loaded.metadata.name}`,
    "",
    expandSkillContextVariables(loaded.content, loaded.baseDirectory),
    "",
    `Base directory for this skill: ${loaded.baseDirectory}`,
    "Relative paths in this skill are relative to this base directory.",
    loaded.truncated ? "[Skill content truncated]" : "",
    "</skill_content>",
  ]
    .filter((line) => line.length > 0)
    .join("\n");
};
```

技能目录里的附属文件由模型按“Base directory”自己去读。工具声明为只读、并发安全、无需审批，超时固定 30000 毫秒（`tool/handlers/skill.ts:101`、`tool/handlers/skill.ts:134`）；遥测只记录限定名、插件 ID 与来源，不带正文和描述（`tool/handlers/skill.ts:52`）。它只在技能端口存在时注册（`apps/zcode-cli/packages/core/src/runtime/helpers/runtime-tools.ts:51`），端口的创建条件就是前面表里的两个开关（`apps/zcode-cli/packages/bootstrap/src/app/create-app.ts:745`）。

工具说明里有一条“看到 `<command-name>` 标签说明技能已经加载，不要再调”（`tool/handlers/skill.ts:99`），但 ZCode 的代码里没有任何地方生成这个标签，这一条在 ZCode 里用不上。用户这一侧，`/skill <name> [task]` 并不自己读文件，而是把输入改写成一条提示词，要求模型先用 `Skill` 工具加载该技能再做事（`apps/zcode-cli/packages/cli/src/command-center/slash-commands.ts:218`）。直接输入 `/技能名` 的效果则取决于入口：TUI 与 `-p` 无头模式都只回一句 Unknown command；App 协议（桌面与 Web）找不到同名自定义命令时把原文交给模型，由模型按工具说明自己去调 `Skill`。

```mermaid
flowchart LR
  R["技能根目录<br/>resolveDefaultSkillRoots"] --> D["discoverSkills<br/>按 priority 扫描"]
  D -->|"会话初始化时一次"| L["skills_listing 提醒<br/>名字 描述 路径"]
  L --> M["模型"]
  U["/skill name task"] -->|"改写成提示词"| M
  M -->|"调用 Skill"| LS["loadSkill<br/>重新发现后取第一个同名项"]
  LS -->|"至多 100000 字节"| O["skill_content 包装的正文"]
  O --> M
```

## 自定义命令：位置、名字与 frontmatter

命令根目录的排法与技能完全相同：`~/.zcode/commands`、`~/.agents/commands`，再从工作目录逐级向上到 Git 根，每层 `.zcode/commands` 与 `.agents/commands`，最后是插件的命令目录（`apps/zcode-cli/packages/adapters/src/commands/roots.ts:20`）。有两点不同：bootstrap 没有给命令传额外根目录，所以没有类似 `skills.roots` 的配置（`apps/zcode-cli/packages/bootstrap/src/custom-commands.ts:91`）；同名命令先到先得，后来的记一条 `custom_command_duplicate_name` 警告（`apps/zcode-cli/packages/adapters/src/commands/index.ts:71`），因此同样是用户级命令压过项目级命令。

- **名字**：取相对根目录的路径，目录分隔符换成 `:`，再转小写，`frontend/test.md` 就是 `/frontend:test`（`adapters/src/commands/index.ts:270`）；必须匹配 `^[a-z0-9][a-z0-9_:-]{0,63}$`，文件名里带点或空格的会被拒绝（`adapters/src/commands/index.ts:22`）。
- **扫描**：递归扫描，深度上限 12，跟随符号链接，悬空链接跳过（`adapters/src/commands/index.ts:25`、`adapters/src/commands/index.ts:242`）；单个文件最多读 100000 字节（`adapters/src/commands/index.ts:23`）。
- **描述**：取 frontmatter 的 `description`，没有就取正文第一行非空内容，去掉开头的 `#` 与列表符号；两者都没有则丢弃该命令（`adapters/src/commands/index.ts:182`、`adapters/src/commands/index.ts:362`）。

frontmatter 认六个键，其余的记 `custom_command_unknown_frontmatter` 警告但不影响加载（`adapters/src/commands/index.ts:26`、`adapters/src/commands/index.ts:194`）：

| 键 | 实际作用 |
| --- | --- |
| `description` | 补全列表与 `/help` 里的说明 |
| `argument-hint` | 附在用法后面，如 `/review <file>` |
| `skills` | 逗号分隔的技能名；展开时在提示词开头要求模型先用 `Skill` 工具加载它们 |
| `disable-noninteractive` | 只在两处生效：App 协议的 `/` 目录剔除该命令，`zcode commands list` 注上 `(interactive only)` |
| `allowed-tools` | 被解析，在 `zcode commands inspect` 里显示，运行时没有消费者 |
| `model` | 同上，不会切换模型 |

`disable-noninteractive` 的两处消费见 `apps/zcode-cli/packages/bootstrap/src/zcode-protocol/slash-commands.ts:43` 与 `apps/zcode-cli/packages/cli/src/commands-command.ts:176`；`-p` 无头模式里没有找到对应的拦截。`allowed-tools` 与 `model` 在 Claude Code 里是有效的控制键（见 Claude Code 手册的 [Skills](https://daiw.org/manual/claude-code/skills) 与 [Commands](https://daiw.org/manual/claude-code/custom-commands) 两篇），在 ZCode 里只是元数据。

## 参数替换与提示词包装

参数替换在 contracts 里（`apps/zcode-cli/packages/contracts/src/commands/index.ts:138`）：

```ts
export function expandCustomCommandTemplate(input: {
  args: string;
  command: CustomCommandContent;
}): CustomCommandTemplateExpansion {
  const args = input.args.trim();
  const positional = splitCustomCommandArguments(args);
  let usedArgumentsPlaceholder = input.command.content.includes(ALL_ARGUMENTS_TOKEN);
  let body = input.command.content.replaceAll(ALL_ARGUMENTS_TOKEN, args);
  body = body.replace(POSITIONAL_ARGUMENT_PATTERN, (_match, index: string) => {
    usedArgumentsPlaceholder = true;
    const offset = Number(index) - 1;
    return positional[offset] ?? "";
  });

  if (args.length > 0 && !usedArgumentsPlaceholder) {
    body = `${body.trimEnd()}\n\nUser arguments:\n${args}`;
  }

  return {
    argumentCount: positional.length,
    body,
    usedArgumentsPlaceholder,
  };
}
```

`$ARGUMENTS` 换成整串参数，`$1`、`$2` 换成按空白切开的第几个参数，切分支持单双引号与反斜杠转义（`contracts/src/commands/index.ts:191`）；缺位的编号换成空串。模板里一个占位符都没用、用户却给了参数时，参数以 `User arguments:` 一段附在末尾，不会丢。展开后的正文前面再加两行：`Run custom command /名字.` 与 `Command source: scope/source.`，声明了 `skills` 的还有两行加载要求（`contracts/src/commands/index.ts:163`）。最终提交给回合的是这段提示词，界面与历史里显示的仍是用户输入的原文（`apps/zcode-cli/packages/bootstrap/src/app/input-facade.ts:131`）。

展开入口是 bootstrap 的 `resolveZCodeCustomCommandPrompt`（`apps/zcode-cli/packages/bootstrap/src/custom-command-prompt.ts:31`）：保留名直接放过，动态工作流灰度关闭时 `/workflow` 也放过（`custom-command-prompt.ts:42`，这个命令来自内置的 zcode-guide 插件），找不到命令时返回 `undefined`，原文当普通提示词交给模型。它挂在 input facade 上，每条提示词都先过一遍；在它之前还有一道内置的 `/init`，把输入换成一段生成或更新工作区 `AGENTS.md` 的长提示词（`apps/zcode-cli/packages/bootstrap/src/builtin-prompt-command.ts:9`、`create-app.ts:793`），所以名为 `init` 的自定义命令永远不会生效。

## Shell 展开与它的边界

模板里可以写两种 shell 片段：行内是感叹号后接一对反引号包住的命令，块状是以三个反引号加感叹号开头的代码块（`apps/zcode-cli/packages/bootstrap/src/custom-command-shell-expansion.ts:13`）。展开时每段依次执行，用标准输出（去掉末尾空白）替换原文，执行方式如下（`custom-command-shell-expansion.ts:119`）：

```ts
  const result = await input.executionPort.run(
    {
      command: {
        mode: "shell",
        command: input.shellCommand,
      },
      cwd: input.workingDirectory,
      env: createShellExpansionEnv({
        plugin,
        sessionId: input.sessionId,
        workingDirectory: input.workingDirectory,
      }),
      outputLimit: {
        maxBufferBytes: DEFAULT_SHELL_EXPANSION_OUTPUT_BYTES,
        maxInlineBytes: DEFAULT_SHELL_EXPANSION_OUTPUT_BYTES,
        persistOutput: "none",
      },
      timeoutMs: DEFAULT_SHELL_EXPANSION_TIMEOUT_MS,
  // ...
    },
    { signal: input.signal },
  );

  if (result.status === "completed" && (result.exitCode ?? 0) === 0) {
    return result.stdout.text.trimEnd();
  }

  throw new Error(formatShellExpansionError(input.command, input.shellCommand, result));
```

- **限制**：每段超时 30000 毫秒，输出上限 128 KiB（`custom-command-shell-expansion.ts:10`）；任何一段非零退出，整条命令就以 `shell expansion failed` 报错，不会带着半截结果进回合。
- **环境变量**：子进程拿到 `ZCODE_PROJECT_DIR`，有会话时加 `ZCODE_SESSION_ID`，插件命令再加 `ZCODE_PLUGIN_ROOT`、`ZCODE_PLUGIN_DATA`、`ZCODE_PLUGIN_ID`、`ZCODE_PLUGIN_NAME`；除后两个外都有对应的 `CLAUDE_` 别名（`custom-command-shell-expansion.ts:160`）。这些是导出的环境变量，不是文本替换；命令里引用技能目录变量会直接报错，因为命令没有技能上下文（`custom-command-shell-expansion.ts:193`）。
- **不经过权限系统**：展开直接调用执行端口，不走工具执行器里的钩子、权限模式与审批，也不受 plan 模式约束（对照[权限模式与规则](https://daiw.org/manual/zcode/permission)与[执行边界](https://daiw.org/manual/zcode/exec-boundary)）。另外参数替换发生在 shell 展开之前（`custom-command-prompt.ts:59`），写在 shell 片段里的 `$1` 会原样拼进命令行。

三个入口对 shell 展开的态度并不一致。bootstrap 的解析器总带着执行端口（`create-app.ts:808`），所以 App 协议与 `-p` 无头模式都会执行；无头模式为了不把用户的 shell 片段跑两遍，探测命令是否存在时只读文件、不展开（`apps/zcode-cli/packages/cli/src/prompt-command.ts:464`）。TUI 空闲时提交走 CLI 自己的展开函数，遇到这两种语法直接报“Dynamic expansion is not available yet”（`apps/zcode-cli/packages/cli/src/custom-command-expand.ts:26`）；可一旦回合正在运行，TUI 改走 `sendInput` 把原文交给 App，同一条命令反而会被执行（`apps/zcode-cli/packages/cli/src/tui-prompt-handler.ts:338`）。

<Callout type="warn">
  仓库里的 `.zcode/commands` 与 `.agents/commands` 属于项目内容。克隆一个陌生仓库后，只要输入了其中某条命令的名字，它模板里的 shell 片段就会以当前用户身份执行，事先不会弹出任何审批；从代码看，这里没有类似工作区钩子那样的信任闸门（钩子的信任机制见[生命周期 Hooks 与工作区信任](https://daiw.org/manual/zcode/hooks)）。
</Callout>

## 内置斜杠命令与三个入口

内置命令只有一张表：`BUILTIN_ZCODE_SLASH_COMMAND_HELP_ENTRIES`（`packages/shared/src/zcode-slash-command-help.ts:9`），`/help` 的文本、TUI 的补全、保留名单都从它生成。19 条按在 TUI 里的去向分组：

| 去向 | 命令 |
| --- | --- |
| TUI 本地处理，不进模型 | `/help`、`/login`、`/logout`、`/effort`（别名 `/variant`）、`/locale`（`/language`）、`/mode`、`/model`、`/mcp`、`/plugins`（`/plugin`）、`/new`（`/clear`）、`/resume`（`/continue`）、不带参数的 `/skill` |
| 改写成提示词再提交 | `/init`、`/skill <name> [task]` |
| 交给回合，在运行时里再识别 | `/compact`、`/rewind`、`/fork` |
| 专门的处理器 | `/goal`（`/target`）、`/expert`、`/dwf` |

TUI 的分派在 `apps/zcode-cli/packages/cli/src/command-center/create.ts:38`：先用 `parseSlashCommand` 识别内置名字（`command-center/slash-commands.ts:17`），认不出的当成自定义命令，再找不到才报 Unknown command（`create.ts:63`）。补全列表是内置条目加自定义命令，技能不在其中（`command-center/slash-commands.ts:238`）。`/compact`、`/rewind`、`/fork` 在回合入口由运行时自己解析（`apps/zcode-cli/packages/core/src/runtime/methods/turn.ts:105`，解析器在 `apps/zcode-cli/packages/core/src/runtime/helpers/commands.ts:5`），各自的机制见[上下文压缩](https://daiw.org/manual/zcode/compaction)与[检查点、回退与分叉](https://daiw.org/manual/zcode/rewind-fork)。

App 协议（桌面与 Web）是另一个面：`/` 目录里的内置命令只有 `goal`、`compact`、`init`，外加只给 App 输入框用的 `plan`（`apps/zcode-cli/packages/bootstrap/src/slash-command-surface.ts:3`）；自定义命令去掉 `disable-noninteractive` 与保留名后附在后面，来自 zcode-guide 的 `/workflow` 被钉在 `goal` 之后（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol/slash-commands.ts:27`、`zcode-protocol/slash-commands.ts:67`）。保留名单是全部内置名字与别名，再加 `compress` 和 `plan`（`slash-command-surface.ts:19`），自定义命令起了这些名字，在 bootstrap 这一层永远不会展开。桌面快捷键表里的“Command Center”（默认 `CmdOrCtrl+k`）是另一个东西，是界面上的命令面板（`packages/shared/src/shortcutCommands.ts:63`），与这里的斜杠命令分发器无关。

```mermaid
flowchart TD
  A["TUI 空闲提交"] --> CC["command center<br/>parseSlashCommand"]
  B["-p 无头"] --> HR{"expert 或 goal<br/>或非自定义命令的未知名字"}
  HR -->|"是"| CC
  HR -->|"否"| IF
  C["TUI 忙碌时的 sendInput<br/>App 协议的 prompt"] --> IF["input facade<br/>customCommandPromptResolver"]
  CC -->|"内置命令"| LOC["本地处理<br/>或改写成提示词"]
  CC -->|"未知名字"| CX["CLI 侧展开<br/>含 shell 语法即报错"]
  LOC -->|"改写后的提示词"| IF
  CX --> IF
  IF -->|"/init"| INIT["内置 AGENTS.md 提示词"]
  IF -->|"自定义命令"| EXP["参数替换 → shell 展开 → 包装"]
  IF -->|"其他"| RAW["原文"]
  INIT --> RT["executeTurn<br/>再识别 /compact /rewind /fork"]
  EXP --> RT
  RAW --> RT
```

## 仓库里的 `.agents/skills` 与产品的内置技能

仓库根目录的 `.agents/skills` 有 8 个技能：`agent-browser`、`ai-elements`、`architecture-governance`、`dep-refs`、`dogfood`、`electron`、`feature-boundary-planner`、`react-best-practices`，其中几个注明派生自 Vercel 的开源项目。它们是给**在本仓库里干活的 Agent** 用的开发工具，`AGENTS.md` 要求改代码前先用 `architecture-governance` 做架构检查（`AGENTS.md:40`）。因为 ZCode 本身就读项目的 `.agents/skills`，在这个仓库里开 ZCode 会话也能看到它们，但它们不随产品发布。

产品自带的技能全部来自官方插件：Browser Use 插件的 `control-browser` 与 `web-gui-tester`（`apps/zcode-cli/packages/browser-use-plugin/skills/`）；文档、PDF、演示文稿、电子表格四个插件各带一个 `docx`、`pdf`、`pptx`、`xlsx` 技能（`apps/zcode-cli/packages/bootstrap/src/app/official-plugin-definitions.ts:175`）；`plugin-creator` 与 `skill-creator` 带同名技能（`official-plugin-definitions.ts:266`、`packages/ui/src/lib/builtinSkillI18n.ts:120`）；zcode-guide 带配置指南、自诊断技能与 `dynamic-workflows` 编写指南，外加 `/workflow` 命令（`official-plugin-definitions.ts:82`、`official-plugin-definitions.ts:297`）；默认关闭的两个模拟器插件分别带 `ios-dev` 与 `android-dev`（`builtinSkillI18n.ts:88`、`builtinSkillI18n.ts:45`）。开源仓库里只有 `browser-use-plugin` 与 `node-repl-host` 两个插件包，其余只有定义没有内容；`superpowers-plugin` 目录下只剩一份 LICENSE，第三方清单写明原先内置的插件实现已被移除（`third-party/copied-components.json:241`）。桌面端还能把本机的用户级技能打包同步到远程工作区（`packages/services/src/skill-sync/skillSync.ts:11`），见[远程工作区与手机远控](https://daiw.org/manual/zcode/remote)。

下一篇：[插件与官方市场](https://daiw.org/manual/zcode/plugins)——插件能带来哪些组件、清单怎样解析、官方市场的内置与 CDN 两个分区，以及插件从来源到运行时的整条装载链。
