Claude CodeClaude ModsAI AgentTypeScriptSecurity

用 Claude Code mod 做一個「工作佇列」面板:哪條線在跑、誰剛交件、什麼在等我決定

··閱讀約 13 分鐘
Claude Code v2.1.302 的畫面:左側逐字稿是 /queue 回的文字結果,右側 Pane 列出 2 個在跑的 task、4 個最近完成(1 個失敗)與 2 件等你決定的事
輸入 /queue 後,左邊是指令回的文字結果,右邊是 Pane。這張圖是在 pty 裡執行真的 Claude Code,再用終端模擬器(pyte)重繪成圖,不是終端機截圖。拍攝時把範例時間改成最近幾小時;直接用範例檔,時間會顯示很久以前。

mod 是什麼

mod 是一種 Claude Code plugin。它用 JavaScript/TypeScript 寫事件處理函式,可以改 Claude Code 的畫面與行為。

官方定義原文:「A mod is a plugin that changes how Claude Code looks and behaves.」

項目內容
推出日期2026-10-01,CHANGELOG v2.1.287 加入
預設開啟官方文件寫「Mods are on by default」,終端機 v2.1.287 以上預設開啟
API 狀態仍在變動。官方 GitHub 的 mods/ README 標「Early access」,寫明 API 可能在版本之間改變
本文依據Claude Code v2.1.302。新版可能要調整
mod 的現況

兩個用語先講清楚,官方文件也這樣分:

hook:mod 裡的事件處理函式。例如「session 開始時做某事」。

settings hook:寫在 settings.json 的舊式 hook,是另一個功能。本文說的 hook 都是前者。

官方 Agent View 看程序,這個 mod 看工作

官方已有 Agent View(claude agents)。它列出同一台機器上的背景 session:哪個在跑、哪個在等你回覆。官方文件標它為 research preview。

Agent View 看的是「程序」。我想看的是「工作」:一條工作線今天跑了哪些 task,哪個失敗了;哪些問題卡在等我決定,提問方建議怎麼做。

這些資訊跟程序在不在跑無關。task 可能由 claude -p 腳本、排程、別的工具產生。

所以這個 mod 不去找程序。它只讀兩個檔:queue.jsonl 在每個 task 開始、結束各寫一行;decisions.json 放目前等你決定的事。

任何腳本都能寫這兩個檔。檔案就是介面。

這個 mod 做什麼

功能怎麼出現內容
Pane(面板)輸入 /queue 打開,在逐字稿旁邊在跑的 task、最近完成的 6 個、等你決定的事
提示上方一行(官方稱 AbovePrompt)有 task 在跑或有待決時自動出現例如「● 2 個 task 在跑(docs frontend) · 2 件等你決定 · /queue 看細節」
/queue 指令打開面板,同時回一段文字跟面板相同的內容。沒有畫面的 claude -p 也看得到這段文字
定時重讀預設每 15 秒重讀兩個檔,面板跟著更新
work-queue-pane 把兩個檔的內容畫在四個地方
Claude Code 提示輸入框上方的一行摘要:● 2 個 task 在跑(docs frontend) · 2 件等你決定 · /queue 看細節
提示上方那一行(AbovePrompt)。這張圖同樣是在 pty 裡執行真的 Claude Code,再用終端模擬器(pyte)重繪成圖。

/queue 是 mod 註冊的 slash command。官方文件說,mod 的指令直接跑函式,不經過 Claude 的回合。

這個 mod 不產生資料。你的腳本或 agent 要自己寫這兩個檔。

資料格式

欄位型別必填說明
ev"start" 或 "end"是task 開始或結束
idstring是同一次執行的 start 與 end 用同一個 id
lanestringstart 必填工作線名稱,例如 backend
taskstringstart 必填task 的一句話描述
atstring是ISO 8601 時間,要帶時區
errorboolean否只用在 end;true 代表失敗
queue.jsonl:一行一個 JSON 物件(JSON Lines),只往後加,不改舊行

有 start、沒有 end 的 task 算「在跑」。解析失敗的行會略過,例如寫到一半的行。

從 shell 腳本寫一行:

bash
echo '{"ev":"start","id":"t7","lane":"docs","task":"update FAQ","at":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}' >> .work-queue/queue.jsonl
欄位型別必填說明
idstring是識別碼
titlestring是一句標題
askstring是要你回答的問題
recstring否提問方的建議
lanestring否哪條工作線在問
decisions.json:一個 JSON 物件,items 是陣列。整檔覆寫;決定完就刪掉那一項
json
{
  "items": [
    { "id": "d1", "lane": "backend", "title": "Rate limit value",
      "ask": "Allow 5 or 10 login attempts per minute?",
      "rec": "10, then lower it if abuse shows up in logs" }
  ]
}

讀不到某個檔時,面板與 /queue 會寫「找不到 <路徑>」。mod 不會假裝沒事。

三個路徑與間隔可以在 /config 改(plugin 的 userConfig):queueFile、decisionsFile、refreshSeconds。相對路徑從 session 的工作目錄算起。

程式怎麼寫:三段關鍵程式

完整程式只有一個檔:hooks/register.tsx,約 200 行。下面挑三段,每段講一個概念。

第一段:register 是 mod 的進入點,在這裡掛 hook。

tsx
export const register: Register = (on, options) => {
  const queueFile = String(options.queueFile ?? '.work-queue/queue.jsonl')
  const decisionsFile = String(options.decisionsFile ?? '.work-queue/decisions.json')
  const refreshMs = Math.max(5, Number(options.refreshSeconds ?? 15)) * 1000

  on('session.start', async ($, e, next) => {
    await $.command.register({
      name: COMMAND,
      description: '打開工作佇列面板:各工作線在跑什麼、等你決定的事',
    })
    await reload($, queueFile, decisionsFile)
    $.clock.every(refreshMs, () => reload($, queueFile, decisionsFile))
    return next(e)
  })
  // ……其餘 hook
}

概念:mod 是一組 hook。Claude Code 載入 mod 時呼叫 register。

on(事件名, 處理函式) 掛一個 hook。options 是使用者在 /config 設的值。$ 是 mod 能用的 API;本 mod 用到讀檔、註冊指令、計時、開面板、讀寫狀態。

next(e) 把事件交回原本的流程。本 mod 只是「順便」做事,不改變 session 開始的行為。

reload 讀兩個檔,把結果寫進 mod 自己的狀態(state)。我第一版把 reload 寫在 register 裡面,validate 不通過。引擎要求接收 $ 的函式宣告在檔案頂層,路徑改用參數傳入。

第二段:ui.render Pane,把資料畫成面板。

tsx
on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
  const { Box, Text } = $.ui.resolve(e)
  const list = await read($, runs)
  const asks = await read($, decisions)
  const now = await $.clock.now()
  const running = list.filter(r => r.endedAt === null)

  return (
    <Box flexDirection="column">
      <Text bold>工作線</Text>
      {running.map(r => (
        <Text>
          <Text color="green">● {r.lane}</Text> {r.task}
          <Text dimColor>  {ago(r.startedAt, now)}</Text>
        </Text>
      ))}
      {/* ……最近完成、等你決定 */}
    </Box>
  )
})

概念:畫面也是 hook。Claude Code 要畫某個元件時,會發出 ui.render 事件。

第二個參數是過濾條件,這裡只接「id 為 work-queue 的 Pane」。$.ui.resolve(e) 拿到這個介面能用的元件,例如 Box、Text。回傳 JSX,Claude Code 就把它畫出來。

提示上方那一行用同一招,條件改成 { component: 'AbovePrompt' }。沒有東西要顯示時,它回傳 next(e),讓原本的畫面照常出現。

第三段:command.run 處理 /queue 指令,有畫面就開面板,沒畫面就回文字。

tsx
on('command.run', { command: COMMAND }, async $ => {
  await reload($, queueFile, decisionsFile)
  try {
    await $.ui.open({ id: PANE, title: TITLE })
  } catch {
    // 沒有畫面的 session(claude -p、雲端)開不了面板;文字結果照樣回。
  }
  const now = await $.clock.now()
  return { text: summaryText(await read($, runs), await read($, decisions), now, await read($, missing)) }
})

概念:指令要替「沒有畫面」的地方留後路。claude -p 與雲端 session 會跑 hook,但不畫面板。

$.ui.open 在沒畫面的地方會失敗,所以包在 try 裡。不管面板開不開得了,指令都回傳 { text }。有畫面時,你同時看到面板和文字;沒畫面時,你至少拿得到文字。

安全:mod 不在沙盒裡

先講結論:裝任何 mod 之前,先跑 claude plugin validate,看 hooks: 和 calls: 兩行。

原因是 mod 用你的權限執行。官方文件原文:「Mods aren't sandboxed.」官方文件也寫明,mod 能讀環境變數裡的 API key,也能自動核准工具呼叫。

claude plugin validate 不執行 mod。它只讀原始碼,列出 mod 掛了哪些事件、呼叫了哪些 API。

下面是本 mod 的實際輸出。我只把本機絕對路徑換成相對路徑:

text
Validating plugin manifest: work-queue-pane/.claude-plugin/plugin.json

  ❯ types ./types/index.d.ts declares on $: nothing (no EngineInterface member)
  ❯ types ./types/index.d.ts declares state: work-queue-pane.runs, work-queue-pane.decisions, work-queue-pane.missing

Validating hooks: work-queue-pane/hooks/hooks.json

  ❯ ./register.tsx hooks: session.start, command.run{command=queue}, ui.render{component=Pane, requestId=work-queue}, ui.render{component=AbovePrompt}
  ❯ ./register.tsx gating hook without .catch: command.run{command=queue}
  ❯ ./register.tsx calls: $.clock.every, $.clock.now, $.command.register, $.fs.read (via reload), $.state.get, $.state.set, $.ui.open, $.ui.resolve
  ❯ ./register.tsx state writes: work-queue-pane.decisions, work-queue-pane.missing, work-queue-pane.runs
  ❯ ./register.tsx state reads: work-queue-pane.decisions, work-queue-pane.missing, work-queue-pane.runs

ℹ Advice (does not change the verdict):

  ❯ work-queue-pane/README.md: This README has no line that installs "work-queue-pane" from a marketplace. Add the install line, which works in one step on Claude Code 2.1.275 or later. Validate the folder that holds its marketplace.json to have <owner>/<repo> filled in, and the name its marketplace lists it under.
      /plugin install work-queue-pane --marketplace <owner>/<repo>

✔ Validation passed
輸出行意思
declares on $: nothing這個 mod 沒有替 $ 加新的 API 給別的 mod 用
declares state它宣告三格自己的狀態:task 清單、待決清單、找不到的檔
hooks:只掛四個 hook。session.start 註冊 /queue、讀檔、啟動定時重讀;command.run{command=queue} 只接 /queue;ui.render 的 Pane 與 AbovePrompt 只畫自己的面板與那一行。清單裡沒有 tool.call 之類的工具事件,所以它不會攔或核准任何工具呼叫
gating hook without .catch這是提示,不是錯誤。/queue 由這個 mod 接手處理,validate 提醒它沒有接例外。會失敗的 $.ui.open 已經包在 try 裡
calls:最該看的一行,列出 mod 實際呼叫的每個 API。$.fs.read (via reload) 是唯一碰檔案的呼叫,而且只讀,讀的是設定裡指定的兩個檔。其餘是計時、註冊指令、開面板、拿畫面元件、讀寫自己的狀態。清單裡沒有寫檔、連網、跑外部程式的呼叫
state writes/state reads只動自己名下的三格狀態,不碰別的 mod 的狀態
Advice建議 README 補一行 marketplace 安裝指令,不影響結果。這個 mod 目前用 zip 發布,所以沒有這一行
validate 輸出逐行看

如果你看到的 calls: 多出別的 API,代表檔案被改過。不要安裝。

自己驗:從下載到看到畫面

需要 Claude Code v2.1.287 以上。

1. 下載 work-queue-pane-0.1.0.zip:

下載 work-queue-pane 0.1.0 原始碼與範例資料(ZIP)

這個 zip 以 MIT 授權發布,全文在 zip 內的 LICENSE。

2.對雜湊值。輸出要等於下面這串;不相等就停下來,不要往下做:

bash
shasum -a 256 work-queue-pane-0.1.0.zip
# 576144e65fb89ea24372fc73ded52baa2ee460e2ec01d4cc8483fcdfabc62dff

3. 解壓。zip 內有外層資料夾,會解出 work-queue-pane/:

bash
unzip work-queue-pane-0.1.0.zip

4. 跑 validate,對照上一節的輸出:

bash
claude plugin validate ./work-queue-pane

5. 建一個試用資料夾,放進範例資料:

bash
mkdir queue-demo
cp -R work-queue-pane/examples/.work-queue queue-demo/
cd queue-demo

6. 在試用資料夾載入 mod,啟動 Claude Code:

bash
claude --plugin-dir ../work-queue-pane

7. 看提示上方那一行。輸入 /queue,打開面板。

範例資料有 3 條工作線、2 個 task 在跑、2 件待決。範例的時間是固定的,所以「跑多久」會顯示很大的數字。這是正常的。

要接你自己的資料,讓你的腳本寫 .work-queue/queue.jsonl 與 .work-queue/decisions.json。或在 /config 把路徑指到別處。

介面hook 會跑畫面會顯示
終端機 claude(含編輯器內建終端機、JetBrains plugin)是是,需 v2.1.287 以上
桌面 App 的 Code 分頁是是,終端機專用元件除外;需 v2.1.286 以上
桌面 App 的 WSL session否否
VS Code 擴充的聊天面板是擴充版本 2.1.299 以上,且 session 要能取得 feature flag
claude -p、Agent SDK是否
雲端 session是(plugin 要能帶到雲端)否
哪些地方看不到畫面(依官方文件的介面表)

看不到畫面的地方,用 /queue 的文字結果。在含 .work-queue/ 的目錄執行 claude -p "/queue",會印出完整文字結果。完整指令是 claude -p "/queue" --plugin-dir <mod 資料夾>。

本文的圖是在 pty 裡執行終端機版 Claude Code,再用終端模擬器重繪而成。桌面 App 與 VS Code 的顯示,我沒有實測。

限制

面板只顯示最近 30 個 task、最近完成的 6 個。

「跑多久」用本機時鐘算。寫檔的機器時鐘不準,顯示也會不準。

0.1.0 版的畫面文字是繁體中文。

下一篇

寫入 queue.jsonl 的通常是排程腳本。排程跑 agent 會遇到的坑,見下面這篇。task 失敗時記得寫 "error": true,面板才看得到。

排程跑 AI Agent,我踩過的三個坑:exit 0 不代表成功

參考資料