Skip to content

Latest commit

 

History

History
690 lines (543 loc) · 20.9 KB

File metadata and controls

690 lines (543 loc) · 20.9 KB

Plugin System

Qwen Code Rust supports plugins that extend the agent with slash commands, skills, MCP servers, agent definitions, and lifecycle hooks. The plugin format is compatible with Claude Code plugins.

Directory Layout

Plugins are discovered from two locations:

Location Scope Precedence
~/.qcr/plugins/ Global — available in every project Lower
.qcr/plugins/ Project-local — current project only Higher

When a global plugin and a project plugin share the same name, the project plugin takes precedence.

Each plugin is a directory containing a metadata file. All other subdirectories are optional:

my-plugin/
├── .claude-plugin/        ← use this for Claude Code compatibility
│   └── plugin.json
├── commands/              ← slash commands (one .md file per command)
│   ├── deploy.md
│   └── review.md
├── skills/                ← skill instruction files (.md with YAML frontmatter)
│   └── rust-patterns.md
├── agents/                ← sub-agent definitions (.md or .json)
│   └── reviewer.md
├── hooks/
│   └── hooks.json         ← lifecycle hook configuration
├── .mcp.json              ← additional MCP server configs
└── README.md

Qwen Code Rust also accepts .qcr-plugin/plugin.json for qcr-specific plugins that are not intended for use with Claude Code.


plugin.json

Every plugin must have a plugin.json inside .claude-plugin/ or .qcr-plugin/.

{
  "name": "feature-dev",
  "version": "1.0.0",
  "description": "Comprehensive feature development workflow",
  "author": {
    "name": "Alice",
    "email": "alice@example.com"
  },
  "category": "development"
}
Field Required Description
name Yes Unique plugin identifier. Used as namespace prefix for commands and MCP servers.
version No Semver version string.
description No Human-readable description.
author.name No Author display name.
author.email No Author email address.
category No Plugin category (e.g. "development", "productivity").

Unknown fields are ignored, so the schema is forward-compatible.


Slash Commands

Each .md file in commands/ defines one slash command. The filename stem becomes the command name.

commands/deploy.md   →  /deploy
commands/review.md   →  /review

The entire file content is the command prompt. When a user types /deploy, qcr injects the file content as a user message and starts an agent turn.

Argument substitution

Use $ARGUMENTS anywhere in the file to receive the text typed after the command name:

Deploy the application to the `$ARGUMENTS` environment.

Steps:
1. Run the test suite.
2. Build a release artifact.
3. Upload and restart the service.

Invocation: /deploy staging$ARGUMENTS is replaced with staging.

If $ARGUMENTS is absent from the content and the user typed extra text, that text is appended to the content on a new line.

Description

The first non-empty, non-heading line in the file is used as the one-line description shown in help output.

Namespaced resolution

Commands are resolved by short name first. If two plugins define /deploy, typing /deploy shows an ambiguity error listing both plugin-a:deploy and plugin-b:deploy. Use the namespaced form /plugin-a:deploy to be unambiguous.


Skills

Each .md file in skills/ defines a skill. Qwen Code Rust parses YAML frontmatter for metadata and uses the body as the instruction content.

---
name: rust-patterns
description: Idiomatic Rust coding patterns
tags: [rust, patterns]
---

Prefer iterators over explicit loops. Use `?` for error propagation.
Avoid `unwrap()` in library code; prefer `expect()` with descriptive messages.

Skills contributed by plugins are registered into the global SkillManager and are available to the GetSkill tool during an agent session.

Skill precedence

When multiple skills share the same name, the first match in this order wins:

  1. Project plugin skills.qcr/plugins/<plugin>/skills/<name>.md
  2. Global plugin skills~/.qcr/plugins/<plugin>/skills/<name>.md
  3. Project skills.qcr/skills/<name>.md
  4. Built-in skills — compiled into the binary; always available, never overwrite user skills

Built-in skills

The following skills ship with qcr and require no installation:

Name Tags Description
spec-driven-development planning, spec, workflow Structured Requirements → Design → Tasks pipeline. Five phases: Requirements → Design → Tasks → Execution → E2E Verification. Includes auto-continue rules, review checkpoints, Phase 5 e2e-fix verification gate, and tasks.md update protocol.
agent-browser browser, automation, web Headless browser automation via the agent-browser CLI. Covers open, snapshot, click, fill, screenshot, and multi-session workflows.
dogfood qa, testing, browser Systematic exploratory testing of a web app to find bugs and UX issues. Produces a structured report with screenshots and repro steps.
e2e-fix qa, testing, fix, browser Run an end-to-end user flow, find bugs, fix them in code, re-run to verify. Loops until all issues are resolved. Used as the Phase 5 verification gate in spec-driven development.
electron browser, automation, desktop Automate Electron desktop apps (VS Code, Slack, Discord, etc.) via Chrome DevTools Protocol.
slack browser, automation, slack Interact with Slack workspaces using browser automation: read channels, send messages, search conversations.
vercel-sandbox browser, automation, vercel Run agent-browser and Chrome inside Vercel Sandbox microVMs for browser automation from Vercel-deployed apps.

Load a built-in skill inside an agent prompt with:

GetSkill { "name": "spec-driven-development" }
GetSkill { "name": "e2e-fix" }

Multi-spec decomposition: when a request involves 2+ distinct features, the agent creates one spec per feature and runs each through the full five-phase pipeline, including the Phase 5 e2e-fix verification gate, before moving to the next spec. See the system prompt's "Multi-Feature Decomposition & E2E Verification" section.

A user-provided or plugin skill with the same name as a built-in always takes precedence — built-ins are inserted with insert_if_absent and are never overwritten.

Adding a built-in skill (contributors)

  1. Create crates/qcr-core/src/prompts/skill.md (or add a new file for the skill body).
  2. Add an include_str! constant in crates/qcr-core/src/prompts/mod.rs.
  3. Call self.insert_if_absent(Skill { ... }) inside SkillManager::register_builtin_skills() in crates/qcr-core/src/skills/mod.rs.
  4. Add unit tests: happy-path load, precedence (user skill wins), appears in list().

Agents

Each .md or .json file in agents/ defines a sub-agent the plugin contributes. The LLM spawns plugin agents via the Task tool's agent parameter:

{ "task": "Review auth.rs for security issues", "agent": "reviewer" }

When the agent parameter is provided, the named plugin agent's system_prompt, tools restriction, and max_turns are applied to the sub-agent. An explicit tools parameter on the Task call overrides the agent's tool list.

Markdown format (with YAML frontmatter)

---
name: reviewer
description: Thorough code reviewer focused on correctness and style
model: qwen3-coder
tools: [ReadFile, Grep, Glob]
max_turns: 10
---

Review the provided code for bugs, performance issues, and style violations.
Return a structured report with severity levels.

The body (everything after the --- closing fence) becomes the agent's system_prompt. All frontmatter fields except name are optional.

JSON format

{
  "name": "reviewer",
  "description": "Thorough code reviewer",
  "system_prompt": "Review the provided code for bugs...",
  "model": "qwen3-coder",
  "tools": ["ReadFile", "Grep", "Glob"],
  "max_turns": 10
}

Agent fields

Field Type Required Description
name string Yes Unique agent name (used in Task tool's agent parameter)
description string No Human-readable description
system_prompt string No System prompt injected when this agent is spawned (body in .md, field in .json)
model string No Model override (reserved for future use; currently uses session default)
tools array No Tool allow-list. When set, the sub-agent can only use these tools. When absent, all tools are available.
max_turns integer No Max agent loop turns. When absent, uses the default (20).

Name collision

When two plugins define an agent with the same name, the first registered agent wins (global plugins load before project plugins within each scope, sorted by directory name). A warning is logged for collisions.


MCP Servers

A plugin can declare additional MCP servers in .mcp.json. The format matches the mcp_servers section of settings.json:

{
  "lint-server": {
    "transport": "stdio",
    "command": "/usr/local/bin/lint-mcp",
    "args": ["--config", ".lintrc"],
    "auto_approve": false
  },
  "remote-checker": {
    "transport": "http",
    "url": "http://localhost:9100/mcp",
    "auto_approve": true
  }
}

Qwen Code Rust namespaces each server by plugin name to avoid collisions: lint-server from plugin feature-dev becomes feature-dev.lint-server.


Hooks

Hooks let a plugin (or the project itself) run shell commands in response to lifecycle events in the agent loop. The hook format is compatible with Claude Code.

Configuration

Hooks are configured in hooks/hooks.json inside the plugin directory. Project-level hooks (not inside any plugin) go in .qcr/hooks/hooks.json.

{
  "description": "Security and quality gates",
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Shell|WriteFile|EditFile",
        "hooks": [
          {
            "type": "command",
            "command": "${QCR_PLUGIN_ROOT}/hooks/security-check.sh",
            "timeout": 15
          }
        ]
      }
    ],
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "./hooks/inject-context.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "WriteFile|EditFile",
        "hooks": [
          {
            "type": "command",
            "command": "cargo fmt --check",
            "async": true,
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Hook events

Event Fires when Blockable
SessionStart A new session begins No
SessionEnd A session terminates No
UserPromptSubmit The user submits a prompt, before the agent processes it Yes
PreToolUse Before any tool executes Yes
PostToolUse After a tool call succeeds No
PostToolUseFailure After a tool call fails No
Stop The agent loop exits naturally Yes
SubagentStart A sub-agent is spawned via the Task tool No
SubagentStop A sub-agent finishes Yes
PreCompact Before context compaction No
Notification A notification is emitted No
TeammateIdle A teammate agent has no pending tasks and is waiting Yes
TaskCompleted A teammate completes a task via the CompleteTask tool Yes

Blockable events: if a hook exits with code 2, qcr treats it as a block — the triggering action is cancelled and an error message is shown. For non-blockable events, exit code 2 is a warning and execution continues.

Matcher patterns

The optional matcher field is a pipe-separated list of tool names. Omit it (or use "*") to match all tools.

{ "matcher": "Shell" }              // Shell only
{ "matcher": "WriteFile|EditFile" } // write tools only
{ "matcher": "*" }                  // all tools (same as omitting matcher)

Handler fields

Field Type Default Description
type string required "command" (shell) or "markdown" (inject context)
command string Shell command to run (required for type: "command")
content string Markdown to inject (required for type: "markdown")
timeout integer 30 Timeout in seconds before the hook is killed
async bool false Run in the background without blocking the agent

Path substitution

Use ${QCR_PLUGIN_ROOT} in a command string to reference the plugin's root directory. This is replaced at runtime with the absolute path to the plugin directory, allowing hooks to call scripts bundled with the plugin regardless of the working directory.

{ "command": "${QCR_PLUGIN_ROOT}/hooks/check.sh" }

Hook input (stdin)

Every hook receives a JSON object on stdin describing the current event:

{
  "session_id": "session-1234567890",
  "cwd": "/home/user/project",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "tool_name": "Shell",
  "tool_input": { "command": "rm -rf /tmp/build" }
}

Common fields:

Field Description
session_id Unique identifier for this session
cwd Current working directory
permission_mode Approval mode (default, plan, etc.)
hook_event_name Name of the event that fired
tool_name Set for PreToolUse, PostToolUse, PostToolUseFailure
tool_input Tool input JSON — set for PreToolUse, PostToolUse
tool_response Tool output text — set for PostToolUse
error Error message — set for PostToolUseFailure
prompt User prompt text — set for UserPromptSubmit
last_assistant_message Last assistant message — set for Stop, SubagentStop
teammate_name Teammate display name — set for TeammateIdle
task_id Task ID — set for TaskCompleted
task_title Task title — set for TaskCompleted

Hook output (stdout + exit code)

Exit codes:

Exit code Meaning
0 Success. Stdout is optionally parsed as a JSON decision.
2 Block (blockable events only). The triggering action is cancelled.
Any other Non-fatal warning. Execution continues.

JSON stdout (exit 0):

A hook can return a JSON object on stdout to influence qcr's behaviour:

{
  "continue": false,
  "stopReason": "Build failed — fix errors before proceeding"
}
Field Type Description
continue bool Set false to stop the agent after this event
stopReason string Message shown when continue is false
hookSpecificOutput.additionalContext string Markdown injected as context into the conversation
hookSpecificOutput.permissionDecision string "allow" or "deny" for PreToolUse hooks
hookSpecificOutput.permissionDecisionReason string Reason shown when denying a tool call

Example — deny a tool call from a PreToolUse hook:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Shell commands are not allowed in this project"
  }
}

Example — inject context from a SessionStart hook:

{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "## Project Context\nThis is a Rust project using tokio."
  }
}

Async hooks

Set "async": true on a handler to run it in the background. The agent does not wait for it to finish. Async hooks cannot block events or inject context — they are fire-and-forget for side effects like running tests or sending notifications.

{
  "type": "command",
  "command": "cargo test --quiet 2>&1 | tee /tmp/test-results.log",
  "async": true,
  "timeout": 120
}

Markdown inject handler

The "markdown" handler type is a qcr extension. It injects static markdown text as context into the conversation without running any process. Useful for injecting project documentation or instructions at SessionStart.

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "markdown",
            "content": "## Coding Standards\nAlways run `cargo fmt` and `cargo clippy` before committing."
          }
        ]
      }
    ]
  }
}

Agent Teams hook examples

Example — react when a teammate goes idle (TeammateIdle):

{
  "hooks": {
    "TeammateIdle": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo \"Teammate $TEAMMATE_NAME is idle\" >> /tmp/team-status.log"
          }
        ]
      }
    ]
  }
}

TEAMMATE_NAME is set in the hook environment from HookContext.teammate_name.

Example — log task completions and optionally stop the agent (TaskCompleted):

{
  "hooks": {
    "TaskCompleted": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 ~/.qcr/hooks/on_task_complete.py"
          }
        ]
      }
    ]
  }
}

The hook script receives TASK_ID and TASK_TITLE as environment variables. It can return:

{ "continue": false, "stopReason": "All tasks done — shutting down." }

to stop the agent once all tasks are finished.


Project-Level Hooks

Hooks outside any plugin go in .qcr/hooks/hooks.json. They follow the same format as plugin hooks but are not bundled with a plugin — they are project-specific overrides.

.qcr/
├── hooks/
│   └── hooks.json    ← project-level hooks
├── plugins/          ← project-local plugins
│   └── my-plugin/
└── settings.json

Project-level hooks are loaded under the source name "project" and fire alongside plugin hooks for the same event.


Writing a Plugin

Minimal plugin

mkdir -p ~/.qcr/plugins/hello/.claude-plugin

cat > ~/.qcr/plugins/hello/.claude-plugin/plugin.json <<'EOF'
{
  "name": "hello",
  "version": "1.0.0",
  "description": "Example plugin"
}
EOF

Add a slash command

mkdir -p ~/.qcr/plugins/hello/commands

cat > ~/.qcr/plugins/hello/commands/greet.md <<'EOF'
Greet the user warmly. Their name is: $ARGUMENTS

Write a short, friendly greeting addressing them by name.
EOF

Invoke with: /greet Alice

Add a hook

mkdir -p ~/.qcr/plugins/hello/hooks

cat > ~/.qcr/plugins/hello/hooks/hooks.json <<'EOF'
{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "markdown",
            "content": "You are working inside the hello plugin context."
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Shell",
        "hooks": [
          {
            "type": "command",
            "command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"additionalContext\":\"Shell command approved\"}}'"
          }
        ]
      }
    ]
  }
}
EOF

Add a skill

mkdir -p ~/.qcr/plugins/hello/skills

cat > ~/.qcr/plugins/hello/skills/greet-style.md <<'EOF'
---
name: greet-style
description: How to greet users in this project
tags: [style, greet]
---

Always greet users with their preferred name. Use a warm, professional tone.
EOF

Discovery and Loading

On startup, qcr:

  1. Scans ~/.qcr/plugins/ (global scope) then .qcr/plugins/ (project scope).
  2. Treats each immediate subdirectory as a candidate plugin.
  3. Loads the plugin if it contains .claude-plugin/plugin.json or .qcr-plugin/plugin.json.
  4. Directories without a valid plugin.json are silently skipped (not an error).
  5. Deduplicates by name: project plugins shadow global plugins with the same name.
  6. Registers all plugin components: skills into SkillManager, commands into PluginCommandRegistry, hooks into HookRunner, MCP servers into the MCP client manager.
  7. Fires SessionStart hooks; any injected markdown is shown as system context.

Plugin loading is best-effort. A plugin that fails to load (malformed JSON, missing required field) does not prevent other plugins from loading.

Load failures are recorded internally and can be surfaced via qcr plugin list (when that subcommand is available).


Security

Hooks execute arbitrary shell commands with the same permissions as qcr itself. Only install plugins from sources you trust.

  • Do not add plugins that run untrusted scripts.
  • Review hooks/hooks.json and any hook scripts before installing a plugin.
  • Use PreToolUse hooks with exit-code 2 blocking to enforce project-level policies, not as a security boundary.
  • ${QCR_PLUGIN_ROOT} substitution is the only variable expansion performed in command strings — shell variable expansion happens inside the executed command itself, not during substitution.