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:
- Evidence-based, no guessing: API behavior, library capabilities, file paths, and configuration semantics must all be verified.
- Never say "supported" or "can be done" before verification; distinguish between "changed" and "verified" — code that hasn't been run is unverified.
- When uncertain, use
// @TODOplaceholders or ask questions; fabricating functions/APIs is forbidden.
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:
- It proactively says "I haven't verified this, you run it" instead of "implemented."
- It actually uses
gh apito pull GitHub source code for confirmation, rather than answering from memory. - It leaves
@TODOin uncertain places instead of fabricating a plausible function name.
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:
- Results are full of SEO garbage and scraped sites; accurate API definitions are buried underneath.
- Web pages are full of nav bars and script tags, pure noise that burns tokens and interferes with judgment.
- General search also can't distinguish whether you want official docs or code examples.
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 forbidden — gh 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.
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:
- Strong framework lock-in — Once you use it, it's hard to migrate out.
- Single file mixing front and back end —
'use client'everywhere, code boundaries blur into a mess, maintenance is painful. - Many bugs, and several severe ones, e.g. — CVE-2025-29927 is a CVSS 9.1 critical auth bypass: if you do permission checks in middleware, an attacker can bypass it entirely by adding an
x-middleware-subrequestheader. Versions 11.1.4 through 15.2.2 are all affected.
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:
- Script cleans up and rebuilds fixtures itself, so it can be run repeatedly without worrying about leftovers from last time.
- Prints
PASS:/FAIL:, finally summarizesN PASS / M FAIL, glance at the numbers, no need to read logs. - It must run it itself first before telling me the result — otherwise what it gives me is "theoretically should pass."
- Uses real source code, real public APIs, real runtime entry points; doesn't mock out the thing under test.
- Cleans up temporary artifacts after running; if you want to keep them to look at, use
KEEP=1. - Gives me real manual test steps and expected results.
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":
- First judge whether the test has signal; if not, don't write a script.
- Don't write assertions for source code strings, className, icon names, copy text, or import existence.
- Syntax-level issues already covered by typecheck/lint are not wrapped in another script.
- UI visual tweaks, layout look-and-feel, overlay focus, mouse interactions are preferably verified manually or via screenshots.
- If only low-value automation is possible, explicitly state that no new script was added and why.
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:
- Which declaration does this symbol resolve to?
- Which are real references, and which are just text coincidentally matching?
- Who calls this function, and who does it call? (Call hierarchy)
- What is the current type, signature, diagnostics, available Code Actions?
- What is the content in the buffer that hasn't been saved to disk yet?
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) orworkspace_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 Skills — Skills 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"