跪拜 Guibai
← Back to the summary

A Two-Year AI Workflow Finally Works, Thanks to Opus 4.8

My AI workflow took two years to write, and only truly took effect with Opus 4.8

I've always had these lines in my CLAUDE.md:


Previous models would read these lines and still fabricate things. You'd ask "Does this API support X?" and it would confidently say yes, then give you a non-existent method name.

Opus 4.8 is different. It actively doubts itself and repeatedly verifies:

Not a single rule was changed; the model is finally willing to comply.

And once the "no guessing" rule truly takes effect, the first question to answer becomes: If you can't guess, where do you look things up?

So this article starts with how to look up information. All configurations are in beixiyo/dotfiles, and there's a skill list at the end that you can copy and use.

The configuration format below is for Claude Code. If you use other tools, you can use my ai-sync to sync them over in one click, for Codex, OpenCode, Gemini, etc.


1. First, lock down "where to look"

When AI encounters an unfamiliar library, its favorite trick is fabricating APIs. The cure isn't adding more "please verify" lines to the rules — it's locking down the search order and forbidding it from immediately calling the built-in Web-Fetch.

This is the entire purpose of the /search skill. It doesn't produce research content itself; it does one thing: routing.

1. Check docs    context7-mcp      ← First priority, for known library/framework/CLI usage
2. Check repo    github skill(gh)  ← Known owner/repo, need to see source, files, Issues, PRs
3. Search code   gh-grep-mcp       ← Don't know which repo to check, want to see how people actually write it
4. Web search    search-mcp        ← Tutorials, error messages, factual questions
5. Fallback      Web Search        ← All of the above failed, or need real-time news

Five tools, each with a clear boundary:

Tool Purpose When to use
Context7 Latest docs & examples for libraries/frameworks/SDKs/CLIs Known specific library, API, config options, version differences
gh CLI Check GitHub repos, files, branches, Issues, PRs Known repo name, need to see source or browse Issues
gh-grep-mcp Search across all GitHub by code pattern (literal/regex) Don't know which repo to check, want to find real-world project usage
Exa AI-oriented search engine, returns cleaned plain text Tutorials, error messages, factual questions
Web Search Built-in general search All above failed, or need news from today

The division of labor between gh and gh-grep-mcp is key: Use gh when you know where to look, use gh-grep when you don't.

For example, "see how people actually write useEffect cleanup for event listeners" should go through gh-grep's regex search:

(?s)useEffect\(\(\) => {.*removeEventListener      Language filter TSX / JSX

Why not use the built-in Web Search?

Three reasons:

Having a skill isn't enough — the skill has to be remembered before it gets called. So I also nailed it into the development workflow in the global CLAUDE.md:

5. **Research uncertainties**: For uncertain library usage, first call the `search` skill;
   For GitHub projects, first call the `github` skill; for deep understanding of large projects, clone the source code.

One line in the skill's description (so it can be auto-triggered), one line hardcoded in the resident rules (so it's forced to remember even if it doesn't want to). Both ends are covered.

gh is the best GitHub searcher

Install and log into gh first, otherwise it's all talk:

# Mac
brew install gh

# Arch
sudo pacman -S --needed --noconfirm github-cli

# Windows
winget install --id GitHub.cli

# Login
gh auth login      # Interactive, choose GitHub.com → HTTPS → Browser authorization
gh auth status     # Verify; if token expires, use gh auth refresh

The Debian/Ubuntu keyring + apt source command is too long; just copy from the official installation guide.

Don't skip the login step. You can read public repos without logging in, but GitHub REST rate-limits unauthenticated requests.

The /github skill is just a few commands; the core is three lines:

# Read file (GitHub API returns base64)
gh api "repos/{owner}/{repo}/contents/{path}" --jq '.content' | base64 -d

# Read README
gh api "repos/{owner}/{repo}/readme" --jq '.content' | base64 -d

# List directory, names only
gh api "repos/{owner}/{repo}/contents/{path}" --jq '.[].name'

Add these few lines, and you've basically got a repo figured out:

# Get an overview first, don't pull the README directly and blow up the context
gh repo view {owner}/{repo} --json name,description,primaryLanguage,stargazerCount,url

# Last 3 commits, sha / message / author only
gh api "repos/{owner}/{repo}/commits?per_page=3" \
  --jq '.[] | {sha: .sha[0:7], message: .commit.message, author: .commit.author.name}'

# Browse Issues for similar problems
gh issue list --repo {owner}/{repo} --search "{keyword}" --limit 5

The --jq part is the key. The raw JSON from the GitHub API is absurdly large; without filtering, it instantly blows up the context. A contents endpoint response includes a bunch of _links, git_url, download_url besides the content field you actually want.

Windows users, note that you probably don't have jq; Unix-like systems should install it themselves.

Also, remember the universal pattern gh api <endpoint> --jq '.field' — all GitHub REST endpoints can be called this way. Just swap the word for languages, releases, tags, pulls; no need to memorize a dedicated command for each type of information.

The skill also has two constraints: URLs containing ? or {} must be quoted (otherwise the shell errors first); execution of any modification commands is forbiddengh repo edit, gh pr merge, etc., are all off-limits. This skill is read-only.

CLI saves way more tokens than installing a GitHub MCP. MCP loads all tool descriptions, input params, and output params into the context on startup, whereas gh is just one command.

Which MCPs I installed

My MCPs are just these few:

{
  "mcp": {
    // Search trio
    "context7-mcp": { "type": "local", "command": ["npx", "-y", "@upstash/context7-mcp"] },
    "gh-grep-mcp": { "type": "remote", "url": "https://mcp.grep.app" },
    "search-mcp": { "type": "remote", "url": "https://mcp.exa.ai" },

    // Things CLI truly can't do
    "lsp-mcp": { "type": "local", "command": ["vv-mcp"] },
    "db-mcp": { "type": "local", "command": ["npx", "@bytebase/dbhub@latest", "--transport", "stdio", "..."] },
    "figma-mcp": { "type": "remote", "url": "https://mcp.figma.com/mcp" },
  },
}

Note that this list has no GitHub MCP, no filesystem MCP. Not an oversight — the CLI can do all of these things, and more efficiently.

The above is the OpenCode format; for Claude Code, switch to command-line addition:

claude mcp add --scope user lsp-mcp -- vv-mcp # This requires downloading my plugin, nvim/VSCode specific https://github.com/beixiyo/vv-mcp.nvim
claude mcp add --scope user context7-mcp -- npx -y @upstash/context7-mcp

2. Let AI read open-source code itself

This is the most underrated thing, in my opinion.

Most people are still stuck at "let AI guess" or "I'll go flip through the docs and paste it for it." Actually, you can directly have it pull the source code and read it.

The previous section's gh api commands are good for reading one or two files. When you need to read 5+ files, or need to cross-reference across directories, calling gh api one by one is stupid. Just pull the source:

git clone --depth=1 --single-branch --no-tags \
  https://github.com/<owner>/<repo>.git /tmp/fsb-<repo>

What the three parameters do:

Parameter What it does Why
--depth=1 Pull only the snapshot of the latest commit, truncate history You want "what the code looks like now," not how this file was changed three years ago. A decade-old repo can differ by an order of magnitude in download size.
--single-branch Pull only the default branch --depth already implies this (unless explicitly --no-single-branch); writing it out prevents behavior drift if depth is someday removed.
--no-tags Don't pull a single tag Monorepos release each package independently; tags can number in the thousands, all references you'll never look at.

I wrote the selection criteria into the /feasibility skill:

Scenario Recommended method
Read 1~2 files / know exact path gh api ... | base64 -d
Check a library's docs / API search routes to Context7 (authoritative, versioned)
Read 5+ files / cross-directory cross-reference / need to search code within repo Shallow clone to /tmp and read locally
Conceptual questions / find alternative solutions search (Exa retrieval)

Directory naming is unified as /tmp/fsb-<repo>. After finishing, ask whether to clean up.

I used this while writing this article — I had it read the converter source code of my own ai-sync repo, and it casually found a real bug in there: when converting Markdown to TOML, it was turning all backtick inline code into Gemini's shell execution syntax. My own code, and I hadn't noticed.

And it didn't stop at "I think there's a problem here." It wrote a minimal example, ran the real conversion function, and pasted the actual output for me:

Input: Create a component named `$1`
Output: Create a component named !{{{arg1}}}

What's run and what's read differ by an order of magnitude in persuasiveness.

To read web pages, verify UI: /playwright-cli

For the browser side, I previously used Vercel's agent-browser, now switched to playwright-cli.

/playwright-skill

The reason for switching is simple: Vercel products have too many bugs.

By the way, Next.js is also their work, and I really don't like it:

Back to playwright-cli. The key configuration is just one — open must carry --persistent:

playwright-cli open "https://example.com" --persistent

It persists localStorage, cookies, and sessions to the default profile directory, automatically restoring them next time. Log in manually once on a new site, and all subsequent conversations are in a logged-in state, no need to fiddle with state-save / state-load every time.

The workflow is to snapshot to get element refs, then operate by ref:

playwright-cli snapshot          # Get refs like e3, e5
playwright-cli click e3
playwright-cli fill e5 "[email protected]" --submit
playwright-cli eval "document.title"

It's AI-friendly because of determinism: the ref comes from the snapshot, so it doesn't have to guess CSS selectors or stuff the entire DOM into the context.


3. The two skills that save the most mental energy

I've configured over twenty skills. Besides the two search routing ones above, there are two more that are truly high-frequency in daily use and save a huge amount of mental energy.

/feasibility: Argue first, then act

The most valuable one.

The old workflow was: I propose a refactoring need → AI starts writing directly → halfway through, discovers it's architecturally impossible → wasted effort, and polluted the code.

Now, complex refactorings go through feasibility first. It must produce a report before it's allowed to act:

Current state → Problem/Goal → Github open-source references → Candidate solution comparison → Recommendation + amount of change → Feasibility conclusion

A few hard rules:

1. **Align before acting** — Feasibility conclusion + solution choice must wait for explicit user confirmation.
   Writing/editing business code without confirmation is forbidden.
2. **Don't rely on memory** — When involving third-party libraries / open-source implementations / unfamiliar APIs,
   **must** call `search` / `github` skill for verification; fabrication is forbidden.
3. **Evidence-based** — Key conclusions must have verifiable sources (repo URL, file path:line number, doc paragraph).
4. **Quantify impact** — Amount of change, risk, API compatibility must be written with specific ranges,
   no vague words like "very small / relatively large."

In the counter-example list, I explicitly banned these:

❌ Directly providing code implementation — User didn't ask you to act; writing code in the feasibility phase is overstepping.
❌ "Should be fine" "Probably no problem" — Conclusion must be explicit ✅/⚠️/❌.
❌ Skipping open-source research — Unless the user explicitly says "quick estimate based on experience."
❌ Automatically entering implementation — Even if the conclusion is ✅, wait for the user to reply "start work" before acting.

That last one is especially important. Without it, it finishes arguing and casually starts modifying code, wasting all the prior alignment.

/how-to-test: After changes, give me a single runnable command directly

The most annoying part after changing code isn't writing tests, it's having to set up the environment just to verify: create a few fake files, prepare a config, figure out what input to hit it with, then guess what you expect from the output.

It wraps up this entire segment. After changes, what I receive is something like this:

For example, just fixed a pagination out-of-bounds bug, it gives me this:

## Test: Pagination parameter out-of-bounds fix

### Automated verification

```bash
bun run /tmp/paginate-test/run.ts
```

Expected: `4 PASS / 0 FAIL`

- `paginate(list100, { page: 1, size: 10 })` → Items 1~10 Normal path
- `paginate(list100, { page: 0, size: 10 })` → Normalized to page 1 ← This is what was fixed
- `paginate(list100, { page: 999 })` → Returns empty array, no error Boundary
- `paginate([], { page: 1 })` → Returns empty array Empty input

### Manual verification

1. Open list page → Jump to last page → Click "Next Page" again
   Expected: Button grayed out, no blank page appears

Copy that command, hit Enter, look at the numbers. No need to build your own fixtures, no need to think about what to test, no need to judge whether the output is correct — the expectation is already written there.

A few conventions make this "copy-paste and done" work:

The entry point is auto-chosen based on what commands are available on the machine; I don't need to say what to run it with:

Condition Entry Run command
command -v bun run.ts bun run /tmp/<slug>-test/run.ts
command -v node run.js node /tmp/<slug>-test/run.js
command -v python3 run.py python3 /tmp/<slug>-test/run.py
Fallback run.sh bash /tmp/<slug>-test/run.sh

The other half of the rules is reining it in from writing useless tests, otherwise "one command to run" becomes "one command to run a bunch of fake green lights":

The last one is key: allow it to say "this isn't worth testing." Without this outlet, it will force out a fake test to hand in — asserting some className exists, asserting some import wasn't deleted, green ten thousand times over, catching zero bugs.

Why /invoke-plan was downgraded

Over half a year ago, this was the one I valued most — for complex requirements, first generate plan/xxx.md, break big tasks into checklists, come back and update status after each step.

Now this skill's trigger condition has been narrowed by me:

description: Only use when the user explicitly requests a plan, task breakdown, long-term progress tracking, phased acceptance,
or when the task requires maintaining a progress file across multiple rounds.

Originally it was "use for complex requirements."

But now the model's own planning ability has improved. Before, without writing a plan file, it would forget what it changed earlier as it wrote, or changes in file A would overwrite the logic in file B. Now it can handle multi-file changes on its own; forcing a plan file adds an extra layer of overhead.

The only scenarios where it's still useful now are two: spanning multiple rounds, needing to pick up the next day after shutting down, and needing phased acceptance.


4. Let AI use LSP to understand local code, not grep for text

The previous two sections were about reading other people's code; this section is about reading your own code.

What does AI rely on to find symbols? grep.

The problem is grep only recognizes strings. Local variables with the same name, mentions in comments, inside string literals — it counts them all as hits. Conversely, references after a rename, cross-file type inheritance, who calls whom — it can't find them all.

But your editor is clearly running LSP, and it knows everything.

First, clarify what LSP is

LSP = Language Server Protocol, a protocol Microsoft defined for VSCode. What it does is separate "language intelligence" from the editor, turning it into an independent process:

Editor (VSCode / Neovim / Emacs...)   ←── JSON-RPC ──→   language server
  Only handles display: highlighting, popups, navigation                                Only handles answering:
                                                          Where is this symbol defined?
                                                          What is its type?
                                                          Who references it?
                                                          What errors are on this line?

The benefit of separation is N × M becomes N + M: tsserver, rust-analyzer, gopls, pyright are written once, all editors can use them; editors don't need to write a separate analyzer for each language.

So I wrote vv-mcp.nvim (Neovim) and vsc-lsp-mcp (VSCode), exposing the LSP already running in the editor to AI via MCP. My main driver now is Neovim; functionality is basically the same on both sides.

I wrote a separate article about the development process of the VSCode plugin: "Hand-writing LSP MCP to Eliminate LLM Hallucinations, Letting AI-IDE Truly Understand Code".

"Reusing the editor's LSP" isn't laziness; it saves memory and configuration. There are some MCPs on the market that independently run a language server, with seemingly similar functionality.

But that means running two LSP server instances for the same project simultaneously, e.g., tsserver: one for your editor, one for AI. In large projects, LSP is the biggest memory hog; this directly doubles it. Making it a VSCode plugin / Neovim plugin is like freeloading off the cost you've already paid — the added memory is near zero.

This is also the justification for lsp-mcp earning a spot in the MCP list above: it needs to talk to a living editor process.

LSP can answer the kind of questions grep can't:

The last one is easy to overlook but very practical — what AI sees is the current state in my editor, not what's on disk.

In my global CLAUDE.md, it sits alongside the earlier search rule as two parallel lines:

4. **Explore real code**: Prioritize using LSP MCP to locate symbols, definitions, references, call relationships,
   types, and diagnostics; use file search only when config, text, or LSP yields no results.
5. **Research uncertainties**: For uncertain library usage, first call the `search` skill.

LSP first, grep only if not found — reversing the order is as good as not installing it.

Installation is one line (requires downloading my plugin, nvim/VSCode specific https://github.com/beixiyo/vv-mcp.nvim):

claude mcp add --scope user lsp-mcp -- vv-mcp     # Claude Code
codex mcp add lsp-mcp -- vv-mcp                   # Codex

There's a counter-intuitive point in usage: query symbols first, then query positions

In the MCP instructions, I hardcoded a calling discipline:

Pass native absolute paths and 1-based positions. When symbol position is uncertain, first use document_symbols (known file) or workspace_symbols (whole project) to locate, then reuse the returned range start. All write operations must go through preview → apply.

Why take this extra step? Because having AI count line numbers itself is unreliable. The file it read might have been truncated, might be a version from several conversation rounds ago, or it might just remember wrong. You tell it "check the type of the symbol at line 42, column 17," the 42 and 17 it gives are guesses, and the hover it queries is for the neighboring variable.

Whereas the range returned by document_symbols is calculated by the language server itself, directly used as input for the next request, with no model involvement in between. Two calls trade for one hallucination.

Renaming follows the same logic: rename_preview first returns a transaction ID and all positions to be changed; after confirmation, rename_apply; if the file was modified in the meantime, it's directly rejected (stale-edit protection). No chance given to "change 87 files in one step."

Two other design choices

Output is compressed. LSP's raw return can instantly blow up the context — a commonly used function's references start in the hundreds. So the server side first filters, deduplicates, groups, truncates (default max 200 entries), then feeds to the model, and can also be cut into markdown format for direct reading.

The same binary is both MCP server and CLI.

vv-mcp fix src/main.ts     # Apply and save safe LSP fixes
vv-mcp lsp --operation document_symbols --uri /abs/path/src/main.ts --query handleClick
vv-mcp lsp --operation hover --uri /abs/path/src/main.ts --line 42 --character 17

Without a subcommand, it's an MCP server (the client starts it this way); with a subcommand, it runs one request and exits.

And CLI mode doesn't require you to have Neovim open. When no registered instance is found, it pulls up a managed headless Neovim for the target project to answer, automatically exiting after 15 minutes of idle time; when you actually open Neovim to edit this project, the managed instance will actively yield to the interactive instance after a 15-second handshake.

So hooks, CI, any shell script can use LSP, no MCP protocol needed, no need to start the editor first — the auto-formatting hook in the next section is connected this way.


5. Orchestration: Solidify repetitive actions into Hooks

Skills manage "how to think"; Hooks manage "things that must be done every time."

A bunch of files were mentioned scattered above; let's align their locations first (Claude Code's global config is all under ~/.claude/):

~/.claude/CLAUDE.md                   Global resident rules, loaded every conversation
~/.claude/settings.json               Permissions + hook registration + statusline
~/.claude/hooks/                      Hook script bodies
  ├── deny-compound-bypass-ast.ts       PreToolUse: AST parse Bash commands
  ├── post-write-code.ts                PostToolUse: ESLint + LSP formatting
  └── lib/                              tree-sitter engine, shell breakdown, path judgment
~/.claude/skills/<name>/SKILL.md      skill
~/.config/opencode/opencode.jsonc     OpenCode counterpart (MCP / permissions / formatter)

Project-level goes in <project>/.claude/; same-name rules override global. settings.json looks like this (omitting statusline, editorMode, etc., unrelated to this article):

{
  "permissions": {
    "allow": [
      "Bash(*)",
      "Read(*)",
      "Edit(~/tmp/**)",
      "mcp__search-mcp__*",
      "mcp__figma-mcp__*",
      "mcp__context7-mcp__*",
      "mcp__ref-mcp__*",
      "mcp__gh-grep-mcp__*",
      "mcp__db-mcp__*",
      "WebSearch",
      "WebFetch(*)"
    ],
    "defaultMode": "bypassPermissions"
  },

  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|Read",
        "hooks": [
          { "type": "command", "command": "bun run ~/.claude/hooks/deny-compound-bypass-ast.ts" }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          { "type": "command", "command": "bun run ~/.claude/hooks/post-write-code.ts" }
        ]
      }
    ],
    "Stop": [
      { "hooks": [{ "type": "command", "command": "bash ~/.zsh/notify/main.sh 'Claude Code'" }] }
    ],
    "PermissionRequest": [
      { "hooks": [{ "type": "command", "command": "bash ~/.zsh/notify/main.sh 'Claude Code needs you'" }] }
    ]
  }
}

PreToolUse: Replace the entire built-in permission system

Note the "defaultMode": "bypassPermissions" plus "Bash(*)" above.

I turned off Claude Code's built-in permission system.

Not for convenience, but because it matches by command prefix, only looking at the first word — compound commands like cd /other/repo && git push bypass it directly. I've been burned by this many times myself.

So the change: allow everything built-in, then use a PreToolUse hook to parse commands myself and block them. Now this hook uses tree-sitter to parse the AST, not regex — every command node in the syntax tree is pulled out and checked individually, no matter if it's hidden inside &&, ;, pipes, $(...), or subshells.

This is quite long to expand on; I wrote a separate article: "One Hook to Plug Most Permission 'Loopholes' in Claude Code and Codex"

PostToolUse: Auto-formatting

After every Write / Edit, automatically run ESLint and Neovim LSP fixes.

Additionally, I turned off some lint rules to prevent infinite loops where AI changes trigger hook formatting, which triggers more changes.

Bun.spawnSync([
  eslint,
  '--fix',
  '--fix-type',
  'layout,suggestion,directive',
  '--rule',
  'unused-imports/no-unused-imports: off',
  '--rule',
  'unused-imports/no-unused-vars: off',
  '--rule',
  'prefer-const: off',
  filePath,
])

AI writes code step by step. It adds an import this round, writes the function using it next round; declares let this round, writes the reassignment branch next round.

If auto-formatting deletes the "unused import" or changes let to const at this point, next round it has to go back and change it again, fighting back and forth.

So these three must be off. --fix-type also only keeps layout,suggestion,directive, not touching structure.

Also, it simultaneously feeds to vv-mcp fix to auto-fix code:

Bun.spawnSync(['vv-mcp', 'fix', filePath], { cwd, stdout: 'ignore', stderr: 'inherit' })

The benefit is the formatting result is exactly the same as when I manually save in the editor, and it's not limited to JS/TS — any language with LSP configured is covered.

Stop / PermissionRequest: Call me when done

Stop              → Desktop notification "Claude Code"           Task finished
PermissionRequest → Desktop notification "Claude Code needs you" Stuck, needs your confirmation

These two are the configs that improve happiness the most, at near-zero cost. After throwing a long task at it, you can go do other things without staring at the terminal.


6. Don't forget to review the code

Is the code usable after writing?

/code-review I require it to go through six dimensions, and forbid nonsense like "the code is well written":

1. Duplicate code      Same or similar logic appears in multiple places
2. Logic conflicts       Contradictory or mutually canceling logic exists
3. Redundant logic       Unnecessary calculations, judgments, or redundant code
4. SRP violation       One function/class does too many things
5. Module coupling       Dependency relationships are overly complex or tightly coupled
6. Hardcoding/dead code  Magic numbers, unused code, branches that never execute

Graded with 🔴 Severe / 🟡 Warning / 🟢 Suggestion; can't pass if there are red lights. And only observe and analyze, don't modify code until I confirm.

/debug follows one principle: Don't guess, only verify.

Many AIs see an error and start guessing "maybe it's a version issue? maybe an environment issue?" then make you try a bunch of useless commands. So the process is hardcoded: analyze code → if info is insufficient, proactively ask me (env vars, dependency versions, logs) → if still uncertain, add logging and have me rerun → fix after getting output.

This skill also has a heavy mode: spin up a local log server + insert instrumentation points into the code, specifically for catching intermittent and cross-device timing issues. But the first section of the entry is triage — if reading code, running tests, or browser automation can locate it, spinning up a server is forbidden.

Otherwise, every small bug would have to go through "start server → instrument → have me reproduce," more annoying than the bug itself.


7. When context is full, don't clear it; do a handover

Context windows, no matter how large, are finite. Worse is Lost in the Middle — the longer the context, the more attention scatters.

When it starts repeating itself or ignoring requirements you just stated, it's time to reset.

But don't just clear it directly; you'd have to explain the requirements all over again. Use /summary to generate a handover document:

### 1. Background and Goals

### 2. Current Progress and Status

**Special note**: If the current code has logic errors, compilation failures, or runtime exceptions,
please detail the current status and what you believe the cause is.

### 3. Important File References (3-5, don't throw everything in)

### 4. To-Do Items and Next Steps

The line in section 2 "if there are exceptions, state the status and cause" was added later — without it, it especially loves to whitewash during handover, writing a bunch of failing tests as "completed."

After generating, click New Chat, paste it in, and seamlessly pick up.


Appendix: Six terms explained in one go

Before configuring, you have to distinguish these things clearly; many people configure for ages without actually understanding the differences.

Term Essence Pass params Auto-trigger Token cost Used for
Rules Resident prompts Low Global norms, project conventions
Commands Callable prompts None Common tasks, repetitive operations
Skills SOP / domain knowledge Low Standard processes, expertise, script references
Hooks Lifecycle hooks None Automation, quality gates
Agents Sub-agents Low Task decomposition, context isolation
MCP External tool calls High Complex capabilities not achievable locally

A few points that are easy to confuse:

Rules vs Commands — Both are essentially prompts. The difference is Rules are resident, Commands are called by typing / and can pass params. Rule filenames differ by vendor; the universal one is AGENTS.md (Claude Code is CLAUDE.md, Gemini is GEMINI.md).

Commands vs SkillsSkills can be auto-triggered, you don't need to type /; AI reads the description and judges for itself whether to use it. So description is the lifeblood of a Skill; if poorly written, it will never be called.

The cost of MCP — On startup, it loads all tool descriptions, input params, and output params into the context; the more you install, the harder it burns. The list of seven MCPs in section one was trimmed down this way: if it can be solved with CLI and Skills, don't use MCP.

A few hard rules for writing Skills

Check item Requirement
name ≤64 chars, lowercase / digits / hyphens
description Non-empty, ≤1024 chars, third person, clearly state what it does and when to use it
SKILL.md lines Recommended <500 lines
Progressive disclosure Main file holds key points, details thrown into references/, scripts thrown into scripts/
Reference depth Only one level allowed

"Progressive disclosure" is the most critical one: the main file is read in full into the context; detail files are only read when needed. Dumping everything into the main file means burning several thousand extra tokens every conversation.


Copy my homework

All configurations are in beixiyo/dotfiles, skills are under .claude/skills/, copy to ~/.claude/skills/<name>/SKILL.md to use.

Besides the ones covered in the main text, there are these, one sentence each:

Skill The core one
search context7 → gh → gh-grep → Exa → Web Search, five-level routing, not allowed to Google right away
github gh api always with --jq filter, files via base64 -d; any modification commands forbidden
feasibility Report first, then act; conclusion can only be ✅/⚠️/❌; no business code writing until "start work" is heard
how-to-test Give one runnable command + expected numbers; if no signal can be tested, explicitly say "this isn't worth testing"
research Conclusion first, then build concept map; evidence in five tiers, official docs > source/release notes > mainstream project usage > Issues/PRs > blogs
code-review Six dimensions + 🔴🟡🟢 grading, only analyze, don't modify code
debug Don't guess, only verify; if static investigation can locate, spinning up a log server is forbidden
summary Handover doc; compilation failures and hanging tests must clearly state symptoms and reproduction methods
workflow Before parallelizing, calculate "write set"; only allow parallel writes if non-overlapping; if overlapping, read-only parallel, main agent serial writes
invoke-plan Only use when maintaining progress files across multiple rounds or needing phased acceptance
commit First git log --oneline -10 to align with the repo's language and style; don't git add without permission
vim-debug No print, dump to /tmp + vim.inspect; transient bugs use once=true autocmd + defer 200ms
playwright-cli open must carry --persistent; snapshot to get ref then operate, don't let it guess selectors

For those using Codex, OpenCode, I wrote ai-sync to sync over in one click:

npm i -g @jl-org/ai-sync
ai-sync

How it converts Claude skills to each vendor's format, I wrote a separate article: "One Config, Sync to Seven AI CLIs"