> ## Documentation Index
> Fetch the complete documentation index at: https://felimet-hub.jmcores.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 04-2 rules: Modular Rule Files, Path Scoping, and Division of Labor

> `.claude/rules/` splits CLAUDE.md's always-on constraints into modules that activate only when matching files are touched. This unit covers the division of labor between rules and CLAUDE.md, the path-scoped loading mechanism, naming and layered organization, and comparisons with equivalent mechanisms in other tools.

export const ToolCompare = ({lang = "zh", tools = [], dimensions = [], defaultSelected, notes = []}) => {
  const UI = lang === "en" ? {
    allTools: "All",
    recommend: "Recommended",
    noSupport: "N/A",
    minOneToolW: "Select at least one tool",
    mobileLabel: "Tool",
    dimensionLbl: "Dimension",
    notes: "Notes"
  } : {
    allTools: "全選",
    recommend: "推薦",
    noSupport: "無對應",
    minOneToolW: "至少選一個工具",
    mobileLabel: "工具",
    dimensionLbl: "維度",
    notes: "注意事項"
  };
  const ACCENT_L = "#bf7551";
  const ACCENT_D = "#cf8a68";
  const REC_BDR_L = "rgba(191,117,81,0.45)";
  const REC_BG_L = "rgba(191,117,81,0.06)";
  const REC_BDR_D = "rgba(207,138,104,0.45)";
  const REC_BG_D = "rgba(207,138,104,0.08)";
  const looksLikePath = val => typeof val === "string" && (/[/\\.*:]/).test(val);
  const safeTools = Array.isArray(tools) ? tools : [];
  const safeDimensions = Array.isArray(dimensions) ? dimensions : [];
  if (safeTools.length === 0 || safeDimensions.length === 0) return null;
  const allIds = safeTools.map(t => t.id);
  const initSelected = Array.isArray(defaultSelected) && defaultSelected.length > 0 ? defaultSelected.filter(id => allIds.includes(id)) : allIds;
  const [selectedIds, setSelectedIds] = useState(initSelected.length > 0 ? initSelected : allIds);
  const [expandedCell, setExpandedCell] = useState(null);
  const [mobileTool, setMobileTool] = useState(initSelected[0] || allIds[0]);
  const [isDark, setIsDark] = useState(false);
  useEffect(() => {
    const detect = () => setIsDark(document.documentElement.classList.contains("dark"));
    detect();
    const obs = new MutationObserver(detect);
    obs.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"]
    });
    return () => obs.disconnect();
  }, []);
  const toggleTool = id => {
    setSelectedIds(prev => {
      const next = prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id];
      if (next.length === 0) return prev;
      if (expandedCell) {
        const [, cellToolId] = expandedCell.split(":");
        if (!next.includes(cellToolId)) setExpandedCell(null);
      }
      return next;
    });
  };
  const isAllSelected = selectedIds.length === allIds.length;
  const toggleAll = () => {
    if (isAllSelected) {
      setSelectedIds([allIds[0]]);
    } else {
      setSelectedIds(allIds);
    }
    setExpandedCell(null);
  };
  const visibleTools = safeTools.filter(t => selectedIds.includes(t.id));
  const cellKey = (dimId, toolId) => dimId + ":" + toolId;
  const toggleExpand = (dimId, toolId) => {
    const key = cellKey(dimId, toolId);
    setExpandedCell(prev => prev === key ? null : key);
  };
  const renderValue = cell => {
    if (cell.na) {
      return <span className="tc-na">{UI.noSupport}</span>;
    }
    const useMono = cell.mono === true || cell.mono !== false && looksLikePath(cell.value);
    return useMono ? <code className="tc-code">{cell.value}</code> : <span>{cell.value}</span>;
  };
  const css = `
  /* ── 根容器 ── */
  .tc-root {
    --tc-bg:           #FAF8F3;
    --tc-surface:      rgba(0,0,0,0.022);
    --tc-stripe:       rgba(0,0,0,0.028);
    --tc-border:       rgba(0,0,0,0.09);
    --tc-text:         #2b2722;
    --tc-dim:          #6f6a62;
    --tc-faint:        #8a8378;
    --tc-accent:       ${ACCENT_L};
    --tc-accent-bg:    ${REC_BG_L};
    --tc-accent-bdr:   ${REC_BDR_L};
    --tc-expand-bg:    rgba(0,0,0,0.016);
    --tc-code-bg:      rgba(0,0,0,0.055);
    --tc-na-color:     #9a9490;
    --tc-pill-on-bg:   rgba(191,117,81,0.10);
    --tc-pill-on-bdr:  rgba(191,117,81,0.35);
    --tc-pill-on-txt:  #a05c38;
    border: 1px solid var(--tc-border);
    border-radius: 14px;
    background: var(--tc-bg);
    color: var(--tc-text);
    overflow: hidden;
    font-size: 14px;
  }
  .dark .tc-root {
    --tc-bg:           #1b1a18;
    --tc-surface:      rgba(255,255,255,0.03);
    --tc-stripe:       rgba(255,255,255,0.025);
    --tc-border:       rgba(255,255,255,0.08);
    --tc-text:         #e7e3da;
    --tc-dim:          #a8a299;
    --tc-faint:        #706b64;
    --tc-accent:       ${ACCENT_D};
    --tc-accent-bg:    ${REC_BG_D};
    --tc-accent-bdr:   ${REC_BDR_D};
    --tc-expand-bg:    rgba(255,255,255,0.022);
    --tc-code-bg:      rgba(255,255,255,0.08);
    --tc-na-color:     #6e6b65;
    --tc-pill-on-bg:   rgba(207,138,104,0.12);
    --tc-pill-on-bdr:  rgba(207,138,104,0.35);
    --tc-pill-on-txt:  ${ACCENT_D};
  }

  /* ── 精簡篩選列 ── */
  .tc-filter-bar {
    display: flex;
    align-items: center;
    gap: 0;
    padding: 0 14px;
    border-bottom: 1px solid var(--tc-border);
    background: var(--tc-surface);
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    /* 單行、不折行，高度由內容決定（約 36px） */
    flex-wrap: nowrap;
    white-space: nowrap;
    min-height: 36px;
  }
  .tc-filter-bar::-webkit-scrollbar { display: none; }

  /* 分隔竿（All 後面） */
  .tc-filter-sep {
    flex-shrink: 0;
    width: 1px;
    height: 14px;
    background: var(--tc-border);
    margin: 0 10px 0 6px;
    align-self: center;
  }

  /* pill 基底 — 極輕量文字標籤 */
  .tc-pill {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 5px 9px;
    border-radius: 5px;
    border: 1px solid transparent;
    background: transparent;
    color: var(--tc-faint);
    font: inherit;
    font-size: 12px;
    font-weight: 500;
    letter-spacing: 0.01em;
    cursor: pointer;
    transition: color 0.12s, background 0.12s, border-color 0.12s;
    flex-shrink: 0;
    white-space: nowrap;
    /* 行高對齊 filter-bar */
    margin: 5px 1px;
  }
  .tc-pill:hover {
    color: var(--tc-text);
    background: rgba(0,0,0,0.04);
  }
  .dark .tc-pill:hover {
    background: rgba(255,255,255,0.05);
  }
  /* 選中態：細框 + 淡底 + 文字加深（非按鈕感，保持輕量） */
  .tc-pill-on {
    color: var(--tc-pill-on-txt);
    background: var(--tc-pill-on-bg);
    border-color: var(--tc-pill-on-bdr);
    font-weight: 600;
  }
  /* 全選 pill — 略小一點字型 */
  .tc-pill-all {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    padding: 4px 8px;
    color: var(--tc-dim);
  }
  .tc-pill-all.tc-pill-on {
    color: var(--tc-pill-on-txt);
  }
  /* 選中小圓點 */
  .tc-pill-dot {
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: var(--tc-accent);
    flex-shrink: 0;
  }

  /* ── 桌面表格 ── */
  .tc-table-wrap {
    overflow: hidden;
  }
  /* Mintlify 的 MDX 渲染器會自動把 <table> 包進 data-table-wrapper：
   * 加 -mx-[var(--page-padding)] 負邊距 + w-[calc(100%+padding*2)] 全寬 + py-[1em]，
   * 讓表格往外溢出，撐破 tc-root 的圓角容器（2026-06-12 線上實證跑版）。
   * 中和它：邊距歸零、寬度收回 100%、把橫向捲動容器設在這層（sticky 左欄靠它）。 */
  .tc-root [data-table-wrapper] {
    margin: 0 !important;
    width: 100% !important;
    max-width: 100% !important;
    padding: 0 !important;
    overflow-x: auto;
    contain: none !important;
  }
  .tc-root [data-table-wrapper] > div {
    padding: 0 !important;
    margin: 0 !important;
  }
  .tc-root [data-table-wrapper]::-webkit-scrollbar { height: 4px; }
  .tc-root [data-table-wrapper]::-webkit-scrollbar-thumb { background: var(--tc-border); border-radius: 2px; }
  .tc-table {
    width: 100%;
    border-collapse: collapse;
    margin: 0 !important;
  }

  /* 表頭 */
  .tc-thead th {
    padding: 9px 15px;
    text-align: left;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--tc-faint);
    border-bottom: 2px solid var(--tc-border);
    background: var(--tc-surface);
    white-space: nowrap;
  }
  /* 維度欄（sticky 左欄） */
  .tc-thead th:first-child,
  .tc-td-dim {
    position: sticky;
    left: 0;
    z-index: 1;
  }
  .tc-thead th:first-child {
    width: 160px;
    min-width: 130px;
    background: var(--tc-surface);
    border-right: 1px solid var(--tc-border);
    z-index: 2;
  }

  /* 維度標籤欄 */
  .tc-td-dim {
    padding: 13px 15px;
    font-size: 12.5px;
    font-weight: 700;
    color: var(--tc-dim);
    vertical-align: middle;
    white-space: nowrap;
    border-bottom: 1px solid var(--tc-border);
    border-right: 1px solid var(--tc-border);
    background: var(--tc-surface);
  }

  /* 斑馬紋：奇數維度列 */
  .tc-row-even .tc-td-dim,
  .tc-row-even .tc-td {
    background-color: var(--tc-stripe);
  }
  .tc-row-even .tc-td-dim {
    background: color-mix(in srgb, var(--tc-surface) 70%, var(--tc-stripe) 30%);
  }

  /* 資料儲存格 */
  .tc-td {
    padding: 0;
    border-bottom: 1px solid var(--tc-border);
    border-left: 1px solid var(--tc-border);
    vertical-align: top;
    min-width: 150px;
  }
  .tc-td-inner {
    display: flex;
    flex-direction: column;
  }

  /* 主值行（可點擊） */
  .tc-cell-btn {
    display: flex;
    align-items: flex-start;
    gap: 6px;
    width: 100%;
    text-align: left;
    background: transparent;
    border: none;
    padding: 13px 15px;
    color: inherit;
    font: inherit;
    font-size: 13px;
    line-height: 1.5;
    cursor: pointer;
    transition: background 0.11s;
    -webkit-tap-highlight-color: transparent;
  }
  .tc-cell-btn:hover {
    background: rgba(0,0,0,0.03);
  }
  .dark .tc-cell-btn:hover {
    background: rgba(255,255,255,0.03);
  }
  .tc-cell-btn-on {
    background: var(--tc-expand-bg);
  }

  /* 推薦標記：左 3px accent 邊框 + 淡底 */
  .tc-td-recommend {
    border-left: 3px solid var(--tc-accent-bdr);
    background: var(--tc-accent-bg);
  }
  .tc-td-recommend .tc-cell-btn:hover {
    background: rgba(191,117,81,0.05);
  }

  /* recommend 角標：超小 badge */
  .tc-rec-badge {
    flex-shrink: 0;
    margin-top: 1px;
    font-size: 9px;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--tc-accent);
    border: 1px solid var(--tc-accent-bdr);
    border-radius: 3px;
    padding: 1px 4px;
    white-space: nowrap;
    opacity: 0.85;
  }

  /* na 樣式 */
  .tc-na {
    font-style: italic;
    color: var(--tc-na-color);
    font-size: 12.5px;
    opacity: 0.7;
  }

  /* 等寬值 */
  .tc-code {
    font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", monospace;
    font-size: 12px;
    background: var(--tc-code-bg);
    border-radius: 3px;
    padding: 1px 4px;
    word-break: break-all;
  }

  /* 展開詳情列 */
  .tc-detail-row td {
    padding: 0;
    border-bottom: 1px solid var(--tc-border);
  }
  .tc-detail-cell {
    padding: 10px 15px 12px;
    font-size: 12.5px;
    line-height: 1.65;
    color: var(--tc-dim);
    background: var(--tc-expand-bg);
    border-top: 1px dashed var(--tc-border);
    border-left: 1px solid var(--tc-border);
  }
  .tc-detail-cell:first-child {
    border-left: none;
    font-weight: 700;
    color: var(--tc-faint);
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    vertical-align: top;
    padding-top: 12px;
    background: var(--tc-surface);
    white-space: nowrap;
    width: 160px;
  }
  .tc-detail-recommend {
    border-left: 3px solid var(--tc-accent-bdr) !important;
    background: var(--tc-accent-bg) !important;
  }

  /* 展開箭頭 */
  .tc-chevron {
    flex-shrink: 0;
    margin-top: 3px;
    opacity: 0.35;
    transition: transform 0.14s, opacity 0.14s;
  }
  .tc-chevron-open {
    transform: rotate(90deg);
    opacity: 0.65;
  }

  /* ── 注腳 ── */
  .tc-notes {
    border-top: 1px solid var(--tc-border);
    padding: 10px 16px 12px;
    display: flex;
    flex-direction: column;
    gap: 4px;
  }
  .tc-notes-label {
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--tc-faint);
    margin-bottom: 3px;
  }
  .tc-note-item {
    font-size: 12.5px;
    color: var(--tc-dim);
    line-height: 1.55;
    padding-left: 14px;
    position: relative;
  }
  .tc-note-item::before {
    content: "*";
    position: absolute;
    left: 2px;
    color: var(--tc-faint);
  }

  /* ── 手機模式 ─────────────────────────────────────────── */
  .tc-mobile { display: none; }
  @media (max-width: 700px) {
    .tc-filter-bar { display: none; }
    .tc-table-wrap  { display: none; }
    .tc-mobile      { display: block; }

    .tc-mob-tabs {
      display: flex;
      overflow-x: auto;
      border-bottom: 1px solid var(--tc-border);
      -webkit-overflow-scrolling: touch;
      scrollbar-width: none;
    }
    .tc-mob-tabs::-webkit-scrollbar { display: none; }
    .tc-mob-tab {
      flex: 1 0 auto;
      padding: 10px 16px;
      background: transparent;
      border: none;
      border-bottom: 2px solid transparent;
      color: var(--tc-dim);
      font: inherit;
      font-size: 13px;
      font-weight: 500;
      cursor: pointer;
      white-space: nowrap;
      transition: color 0.12s, border-color 0.12s;
    }
    .tc-mob-tab-on {
      color: var(--tc-accent);
      border-bottom-color: var(--tc-accent);
      font-weight: 700;
    }
    .tc-mob-cards {
      padding: 12px 0 4px;
    }
    .tc-mob-card {
      border-bottom: 1px solid var(--tc-border);
      padding: 12px 16px;
    }
    .tc-mob-card:last-child { border-bottom: none; }
    .tc-mob-dim {
      font-size: 11px;
      font-weight: 700;
      letter-spacing: 0.05em;
      text-transform: uppercase;
      color: var(--tc-faint);
      margin-bottom: 6px;
    }
    .tc-mob-val {
      font-size: 13.5px;
      line-height: 1.55;
    }
    .tc-mob-rec {
      display: inline-block;
      margin-top: 6px;
      font-size: 9.5px;
      font-weight: 700;
      letter-spacing: 0.05em;
      text-transform: uppercase;
      color: var(--tc-accent);
      border: 1px solid var(--tc-accent-bdr);
      border-radius: 3px;
      padding: 1px 5px;
    }
    .tc-mob-detail {
      margin-top: 7px;
      font-size: 12.5px;
      line-height: 1.65;
      color: var(--tc-dim);
    }
    .tc-notes { padding: 11px 16px 13px; }
  }
  `;
  const renderDesktopTable = () => <div className="tc-table-wrap">
      <table className="tc-table">
        <thead className="tc-thead">
          <tr>
            <th>{UI.dimensionLbl}</th>
            {visibleTools.map(tool => <th key={tool.id}>{tool.label}</th>)}
          </tr>
        </thead>
        <tbody>
          {}
          {safeDimensions.flatMap((dim, dimIdx) => {
    const isAnyExpanded = visibleTools.some(t => expandedCell === cellKey(dim.id, t.id));
    const stripeClass = dimIdx % 2 === 1 ? " tc-row-even" : "";
    const rows = [];
    rows.push(<tr key={dim.id} className={"tc-row" + stripeClass}>
                <td className="tc-td-dim">{dim.label}</td>
                {visibleTools.map(tool => {
      const cell = (dim.cells || ({}))[tool.id] || ({});
      const key = cellKey(dim.id, tool.id);
      const isOpen = expandedCell === key;
      const hasDetail = !!cell.detail;
      const isRec = !!cell.recommend;
      return <td key={tool.id} className={"tc-td" + (isRec ? " tc-td-recommend" : "")}>
                      <div className="tc-td-inner">
                        <button type="button" className={"tc-cell-btn" + (isOpen ? " tc-cell-btn-on" : "")} onClick={hasDetail ? () => toggleExpand(dim.id, tool.id) : undefined} style={hasDetail ? {} : {
        cursor: "default"
      }} aria-expanded={hasDetail ? String(isOpen) : undefined}>
                          <span style={{
        flex: "1 1 0",
        minWidth: 0
      }}>
                            {renderValue(cell)}
                          </span>
                          {isRec && <span className="tc-rec-badge">{UI.recommend}</span>}
                          {hasDetail && <svg className={"tc-chevron" + (isOpen ? " tc-chevron-open" : "")} width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
                              <polyline points="9 18 15 12 9 6" />
                            </svg>}
                        </button>
                      </div>
                    </td>;
    })}
              </tr>);
    if (isAnyExpanded) {
      const openToolId = visibleTools.find(t => expandedCell === cellKey(dim.id, t.id))?.id;
      const openCell = openToolId ? (dim.cells || ({}))[openToolId] || ({}) : {};
      if (openCell.detail) {
        const openTool = safeTools.find(t => t.id === openToolId);
        const isRec = !!openCell.recommend;
        rows.push(<tr key={dim.id + "-detail"} className="tc-detail-row">
                    <td className="tc-detail-cell">
                      {openTool ? openTool.label : ""}
                    </td>
                    <td colSpan={visibleTools.length} className={"tc-detail-cell" + (isRec ? " tc-detail-recommend" : "")}>
                      {openCell.detail}
                    </td>
                  </tr>);
      }
    }
    return rows;
  })}
        </tbody>
      </table>
    </div>;
  const renderMobile = () => {
    const activeTool = safeTools.find(t => t.id === mobileTool) || safeTools[0];
    return <div className="tc-mobile">
        <div className="tc-mob-tabs" role="tablist">
          {safeTools.map(tool => <button key={tool.id} type="button" role="tab" aria-selected={tool.id === mobileTool ? "true" : "false"} className={"tc-mob-tab" + (tool.id === mobileTool ? " tc-mob-tab-on" : "")} onClick={() => setMobileTool(tool.id)}>
              {tool.label}
            </button>)}
        </div>

        <div className="tc-mob-cards">
          {safeDimensions.map(dim => {
      const cell = (dim.cells || ({}))[activeTool.id] || ({});
      const isRec = !!cell.recommend;
      return <div key={dim.id} className="tc-mob-card">
                <div className="tc-mob-dim">{dim.label}</div>
                <div className="tc-mob-val">{renderValue(cell)}</div>
                {isRec && <div className="tc-mob-rec">{UI.recommend}</div>}
                {cell.detail && <div className="tc-mob-detail">{cell.detail}</div>}
              </div>;
    })}
        </div>
      </div>;
  };
  return <div className="tc-root">
      <style>{css}</style>

      {}
      <div className="tc-filter-bar">
        <button type="button" className={"tc-pill tc-pill-all" + (isAllSelected ? " tc-pill-on" : "")} onClick={toggleAll} aria-pressed={String(isAllSelected)}>
          {UI.allTools}
        </button>

        {}
        <span className="tc-filter-sep" aria-hidden="true" />

        {safeTools.map(tool => {
    const isOn = selectedIds.includes(tool.id);
    return <button key={tool.id} type="button" className={"tc-pill" + (isOn ? " tc-pill-on" : "")} onClick={() => toggleTool(tool.id)} aria-pressed={String(isOn)}>
              {isOn && <span className="tc-pill-dot" aria-hidden="true" />}
              {tool.label}
            </button>;
  })}
      </div>

      {}
      {visibleTools.length > 0 && renderDesktopTable()}

      {}
      {renderMobile()}

      {}
      {notes.length > 0 && <div className="tc-notes">
          <div className="tc-notes-label">{UI.notes}</div>
          {notes.map((note, i) => <div key={i} className="tc-note-item">{note}</div>)}
        </div>}
    </div>;
};

export const LearnerPrimer = ({items = [], lang = "zh"}) => {
  const t = lang === "en" ? {
    title: "Before this unit, be honest with yourself",
    sub: "If you can't answer these, that gap is exactly what this unit closes."
  } : {
    title: "讀這個單元前，先誠實面對",
    sub: "這幾題答不出來，正是這個單元要替你補的洞。"
  };
  const css = `
  .lp-root{--lp-bg:#FAF7F1;--lp-surface:rgba(191,117,81,0.05);--lp-border:rgba(0,0,0,0.09);--lp-edge:rgba(191,117,81,0.42);--lp-text:#2b2722;--lp-dim:#6f6a62;--lp-accent:#bf7551;border:1px solid var(--lp-border);border-left:3px solid var(--lp-edge);border-radius:13px;background:var(--lp-bg);color:var(--lp-text);overflow:hidden;margin:1.25rem 0;}
  .dark .lp-root{--lp-bg:#1b1a18;--lp-surface:rgba(207,138,104,0.07);--lp-border:rgba(255,255,255,0.08);--lp-edge:rgba(207,138,104,0.5);--lp-text:#e7e3da;--lp-dim:#a8a299;--lp-accent:#cf8a68;}
  .lp-head{display:flex;align-items:center;gap:9px;padding:13px 18px 11px;border-bottom:1px solid var(--lp-border);background:var(--lp-surface);}
  .lp-ic{color:var(--lp-accent);flex-shrink:0;}
  .lp-htx{display:flex;flex-direction:column;gap:1px;min-width:0;}
  .lp-title{font-size:14px;font-weight:650;line-height:1.3;letter-spacing:.01em;}
  .lp-sub{font-size:12px;color:var(--lp-dim);line-height:1.4;}
  .lp-list{list-style:none;margin:0;padding:10px 18px 14px;display:flex;flex-direction:column;gap:0;}
  .lp-item{display:flex;align-items:baseline;gap:11px;padding:7px 0;font-size:14px;line-height:1.6;border-top:1px solid var(--lp-border);}
  .lp-item:first-child{border-top:none;}
  .lp-mark{flex-shrink:0;color:var(--lp-accent);font-size:13px;font-weight:700;line-height:1.55;font-variant-numeric:tabular-nums;opacity:.85;}
  .lp-q{flex:1 1 0;min-width:0;color:var(--lp-text);}
  @media (max-width:620px){.lp-head{padding:12px 14px 10px;}.lp-list{padding:8px 14px 12px;}}
  `;
  return <div className="lp-root">
      <style>{css}</style>
      <div className="lp-head">
        <svg className="lp-ic" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="6" /><circle cx="12" cy="12" r="2" /></svg>
        <span className="lp-htx">
          <span className="lp-title">{t.title}</span>
          <span className="lp-sub">{t.sub}</span>
        </span>
      </div>
      <ul className="lp-list">
        {items.map((q, i) => <li className="lp-item" key={i}>
            <span className="lp-mark">{String(i + 1).padStart(2, "0")}</span>
            <span className="lp-q">{q}</span>
          </li>)}
      </ul>
    </div>;
};

<LearnerPrimer
  lang="en"
  items={[
"rules don't fragment CLAUDE.md -- they scope it by path.",
"Setting paths to **/* is the same as not scoping at all.",
"Cross-domain rules that stay together dilute each other's instructions.",
"A rule is a context layer -- the model can miss it. For guaranteed execution, use a Hook.",
"Rules that never get reviewed are wrong rules within three months.",
"One criterion only: would reading this rule to an unrelated task cause interference?",
"Wide paths mean full load on every session startup.",
]}
/>

<Info>
  **What this unit solves**

  `.claude/rules/` splits `CLAUDE.md`'s always-on constraints into modules that activate only when matching files are touched. When `CLAUDE.md` grows bloated, cross-domain rules start interfering with each other, or certain rules only make sense for specific file extensions, rules are the right answer. This unit covers the division of labor between rules and `CLAUDE.md`, the path-scoped loading mechanism, naming and layered organization, and comparisons with equivalent mechanisms in other tools.
</Info>

## Learning objectives

* [ ] State the difference between rules and `CLAUDE.md`, and know when to split something into rules.
* [ ] Use the `paths` frontmatter to load a rule only when files matching a glob are touched.
* [ ] Split constraints by domain or language into separate rule files, avoiding a single monolithic file.
* [ ] Decide whether a constraint belongs in `CLAUDE.md`, a standalone rule file, a Skill, or a Hook, and state the deciding criterion.
* [ ] Compare the equivalent mechanisms in OpenAI Codex, Google Antigravity, GitHub Copilot, and Cursor.

***

## 1. What rules are

Rules are Markdown files placed in `.claude/rules/`, loaded at the same level as `CLAUDE.md` (as of 2026-06, per the official memory documentation \[1]). Key characteristics:

* Each `.md` file is one module, named after its topic (`testing.md`, `api-design.md`, `python-style.md`).
* Without a `paths` frontmatter field, a rule is equivalent to `CLAUDE.md`: loaded in full at every session start.
* With a `paths` frontmatter field, **the rule is only injected into context when Claude touches a file matching the glob**.
* Like `CLAUDE.md`, rules belong to the "context layer," not "enforced execution." For constraints that must be guaranteed to run, use a Hook (see [04-6 Hooks](/en/code-agent/customization/hooks)).

All `.md` files inside `.claude/rules/` are read recursively; you can organize them into subdirectories (`frontend/`, `backend/`, `ml/`) for a layered hierarchy \[1].

<Tip>
  **Rules are "always-on within a scope," not "called on demand"**

  After adding `paths`, a rule is still injected into context the moment Claude touches a matching file. This differs from Skills, where the full content is only injected when the user or model explicitly invokes it (see [04-4 Skills](/en/code-agent/customization/skills)). Rules are "always resident within a subset of working sessions," not "called once like a tool."
</Tip>

## 2. Division of labor with `CLAUDE.md`

`CLAUDE.md` (user-level, project-level, or local layer) holds constraints that apply **across tasks, languages, and directories**. Rules hold **contextual** constraints: ones that are only meaningful for a specific path or file extension.

There is only one criterion: **read this rule aloud to an agent working on an unrelated task -- would it cause interference or contradictions?** Yes: split it into rules. No: keep it in `CLAUDE.md`.

<Note>
  **How to split**

  Stay in `CLAUDE.md`:

  * "Responses always in Traditional Chinese, second person"
  * "Never commit `.env`"
  * "Build command: `make build`; tests: `make test`"
  * "Run `make lint` before every commit"

  Move to `.claude/rules/python-style.md` (path-scoped to `**/*.py`):

  * "Python requires type hints; public functions must not accept `Any`"
  * "pytest fixtures go in `conftest.py`; do not define cross-file fixtures inside individual test files"
  * "Public API functions must have a docstring (Google style)"

  Move to `.claude/rules/api-design.md` (path-scoped to `src/api/**`):

  * "Every endpoint must be registered in the OpenAPI spec"
  * "Input validation is done at the handler boundary, not inside internal functions"
  * "Error responses always use the `{code, message, details}` structure"

  These three blocks do not interfere with each other: writing React does not load the Python style rules; writing Python does not load the API design rules. The token budget per session is an order of magnitude smaller than stuffing everything into `CLAUDE.md`.
</Note>

## 3. Path-scoped rules: the loading mechanism

`.md` files in `.claude/rules/` can use the `paths` field in YAML frontmatter to restrict when they load. Claude Code only injects the rule into context when **a file matching the glob is read during the session** \[1].

Basic syntax:

```markdown theme={null}
---
paths:
  - "src/api/**/*.ts"
---

# API Development Rules

- All API endpoints must include input validation
- Use the standard error response format
- Include OpenAPI documentation comments
```

Glob syntax (similar to `.gitignore` style):

| Pattern                | Matches                                                            |
| ---------------------- | ------------------------------------------------------------------ |
| `**/*.ts`              | All `.ts` files in any directory                                   |
| `src/**/*`             | All files under `src/`                                             |
| `*.md`                 | `.md` files in the project root only                               |
| `src/components/*.tsx` | `.tsx` files directly in `src/components/` (no subdirectories)     |
| `src/**/*.{ts,tsx}`    | All `.ts` and `.tsx` files under `src/` (including subdirectories) |
| `tests/**/*.test.ts`   | Test files                                                         |

Multiple patterns can be listed together, or brace expansion used to match multiple extensions at once \[1].

<Warning>
  **Path-scoped rules only load when a file is read**

  A path-scoped rule does not load on every tool call that touches matching files -- it loads when Claude reads a file matching the glob. This means: if Claude is asked to edit a file before it has read any file matching the glob, the rule is not in context. Behavior may be inconsistent before that point in the conversation.
</Warning>

## 4. Naming and layered organization

Naming principle: use specific semantic names, not specific behavioral descriptions (not `do-not-touch.md`, but `api-migration-safety.md`).

Common layering:

<Tree>
  <Tree.Folder name=".claude" defaultOpen>
    <Tree.File name="CLAUDE.md" />

    <Tree.Folder name="rules" defaultOpen>
      <Tree.Folder name="common" defaultOpen>
        <Tree.File name="code-style.md · general language style" />

        <Tree.File name="testing.md · testing principles" />

        <Tree.File name="security.md · security prohibitions (path-scoped to src/)" />
      </Tree.Folder>

      <Tree.Folder name="frontend" defaultOpen>
        <Tree.File name="react-style.md · React conventions" />

        <Tree.File name="a11y.md · accessibility" />
      </Tree.Folder>

      <Tree.Folder name="backend" defaultOpen>
        <Tree.File name="api-design.md · API design" />

        <Tree.File name="db-migrations.md · database migrations" />
      </Tree.Folder>

      <Tree.Folder name="ml" defaultOpen>
        <Tree.File name="experiment-tracking.md" />
      </Tree.Folder>
    </Tree.Folder>
  </Tree.Folder>
</Tree>

Single rule file length: the official recommendation for `SKILL.md` is under 500 lines (a guideline for skills, but the same spirit applies to rules). Beyond that length, consider splitting further or upgrading to a Skill.

## 5. Sharing across projects: symlinks

`.claude/rules/` supports symlinks, so you can maintain a shared rule set in `~/shared-claude-rules/` and link it into multiple projects \[1]:

```bash theme={null}
ln -s ~/shared-claude-rules .claude/rules/shared
ln -s ~/company-standards/security.md .claude/rules/security.md
```

Circular symlinks are detected and bypassed; there is no infinite recursion \[1]. On Windows, creating symlinks requires Administrator privileges or Developer Mode. If symlinking is not available, importing `~/shared-*.md` via `@path` inside `.claude/rules/` is an alternative.

## 6. User-level rules

Rules placed in `~/.claude/rules/` apply to **all projects**, with a load order after the user-level `CLAUDE.md` and before project-level rules \[1]:

<Tree>
  <Tree.Folder name="~/.claude/rules" defaultOpen>
    <Tree.File name="preferences.md · personal coding preferences" />

    <Tree.File name="workflows.md · personal common workflows" />
  </Tree.Folder>
</Tree>

Common uses: personal tool shortcuts, editor setting preferences, response style conventions that apply across all projects. Project rules override user rules when a file of the same name exists in both.

## 7. The boundary with Skills and Hooks

The three mechanisms have non-overlapping responsibilities:

| Requirement                                                           | Where it goes                      |
| --------------------------------------------------------------------- | ---------------------------------- |
| "Follow this every time a Python file is edited"                      | rules (path-scoped)                |
| "Let the model automatically invoke this procedure at the right time" | Skill                              |
| "Run tests before every commit without exception"                     | Hook (deterministic guarantee)     |
| "I know I want to run this procedure now"                             | Command (`/name` explicit trigger) |
| "General project knowledge every task should know"                    | `CLAUDE.md`                        |

Quick decision tree:

* Model needs to **judge when to insert** it: Skill
* User needs to **trigger it explicitly**: Command
* Must run **after every occurrence of an action** (regardless of model judgment): Hook
* **Always resident, loaded every session**, applies across tasks: `CLAUDE.md`
* **Always resident, loaded only for specific paths**: rules (path-scoped)
* **Recording facts rather than instructions**: auto memory

## 8. Tool comparison

Rule file mechanisms differ considerably across tools in path-scoped support and frontmatter details (as of 2026-06):

<ToolCompare
  lang="en"
  tools={[
{ id: "claude", label: "Claude Code" },
{ id: "codex",  label: "OpenAI Codex" },
{ id: "gemini", label: "Google Gemini CLI" },
{ id: "copilot",label: "GitHub Copilot" },
{ id: "cursor", label: "Cursor" },
]}
  dimensions={[
{
  id: "rule-location",
  label: "Rule file location",
  cells: {
    claude:  { value: ".claude/rules/*.md",                        recommend: true, detail: "Subdirectories allowed (frontend/, backend/, ml/); loaded recursively." },
    codex:   { value: "Policy files (Starlark *.rules)", detail: "OpenAI Codex rules are executable policy files written in Starlark that control agent behavior via allowlists/blocklists. Semantically closer to Hooks than Markdown rule files." },
    gemini:  { value: "~/.gemini/settings.json, <workspace>/.agent/rules/", detail: "MCP server and tool rules are configured in the mcpServers section of settings.json; custom tool rules can go in .agent/rules/ subdirectories." },
    copilot: { value: ".github/copilot-instructions.md + .github/instructions/*.instructions.md" },
    cursor:  { value: ".cursor/rules/*.mdc", detail: ".mdc is the required extension; .md is ignored. Frontmatter fields: description / globs / alwaysApply." },
  },
},
{
  id: "path-scoped",
  label: "Path-scoped support",
  cells: {
    claude:  { value: "Yes (paths frontmatter, glob)",             recommend: true, detail: "Once paths is set, the rule loads only when Claude reads a file matching the glob -- the core token-saving mechanism. paths: [\"**/*\"] is equivalent to no scoping at all." },
    codex:   { value: "N/A (Starlark is policy logic)", detail: "Starlark policy files handle allowlist/blocklist logic; they are not Markdown rules loaded by path glob." },
    gemini:  { value: "Via multiple .agent/rules/ files and directory structure" },
    copilot: { value: "Yes (applyTo glob, optional excludeAgent)", detail: ".instructions.md frontmatter applyTo restricts to a glob; excludeAgent can exclude specific agents from loading the file." },
    cursor:  { value: "Yes (.mdc frontmatter globs + alwaysApply)", detail: "alwaysApply: true loads unconditionally; globs triggers auto-load on match; nothing set requires manual @-mention." },
  },
},
{
  id: "modular",
  label: "Modular multi-file",
  cells: {
    claude:  { value: "Multiple .md files, subdirectories allowed", recommend: true },
    codex:   { value: "Multiple *.rules files" },
    gemini:  { value: "Multiple files supported" },
    copilot: { value: "Multiple .instructions.md files" },
    cursor:  { value: "Multiple .mdc files" },
  },
},
{
  id: "cross-tool",
  label: "Cross-tool common layer",
  cells: {
    claude:  { value: "Reads CLAUDE.md; @AGENTS.md import available" },
    codex:   { value: "AGENTS.md native" },
    gemini:  { value: "AGENTS.md native" },
    copilot: { value: "AGENTS.md native" },
    cursor:  { value: "AGENTS.md native" },
  },
},
{
  id: "precedence",
  label: "Rule precedence",
  cells: {
    claude:  { value: "User-level loads first; project layer overrides on conflict", recommend: true },
    codex:   { value: "managed (requirements.toml) acts as ceiling" },
    gemini:  { value: "System Rules (DeepMind immutable) > user global > project" },
    copilot: { value: "All matching instruction files merged; no strict precedence" },
    cursor:  { value: "Per .mdc frontmatter settings and official conventions" },
  },
},
]}
  notes={[
"OpenAI Codex 'rules' are executable Starlark policy files, not Markdown rules -- semantically closer to Hooks.",
"GitHub Copilot .instructions.md uses applyTo to restrict by glob, similar in semantics to Claude's paths frontmatter.",
"Cursor is a third-party IDE (Anysphere); this Playbook includes only a brief column for it.",
]}
/>

<Note>
  **Naming clarifications**

  * **OpenAI Codex "rules"** are executable policy files written in Starlark (allowlists/blocklists controlling agent behavior), not Markdown rule files. Semantically they are closer to Hooks than to the `CLAUDE.md` modularization discussed here.
  * **Cursor** uses the `.mdc` extension (`.md` does not work). The frontmatter fields are `description`, `globs`, and `alwaysApply` \[3]. `alwaysApply: true` means unconditional loading; `globs` set means auto-load only on matching files; `description` only means the model decides by semantic relevance; nothing set means manual `@`-mention only.
  * **GitHub Copilot** `.instructions.md` uses `applyTo` to restrict to a glob, and can add `excludeAgent: "code-review"` or `"cloud-agent"` to exclude specific agents from loading it \[4].
</Note>

## 9. Hands-on exercises

<Steps>
  <Step title="Split one rule">
    Move the Python coding style section from `CLAUDE.md` wholesale into `.claude/rules/python-style.md`, adding `paths: ["**/*.py"]`.
  </Step>

  <Step title="Verify loading">
    In the same session, run `/memory` and confirm `python-style.md` appears with its path annotation.
  </Step>

  <Step title="Test the no-load scenario">
    Ask Claude to edit a `*.md` file (outside the `**/*.py` glob) and then ask about Python style rules. It should not reference `python-style.md`.
  </Step>

  <Step title="Cross-project sharing">
    Create `~/.claude/rules/preferences.md`; start sessions in two different projects and confirm it is loaded in both.
  </Step>
</Steps>

## 10. Common pitfalls

<Warning>
  **Anti-pattern list**

  * **Stuffing all constraints into `CLAUDE.md`**: cross-domain rules interfere with each other, every session loads everything, token budget spikes, and long rules are prone to "attention drift" where the model quietly ignores sections.
  * **Copying someone else's rules configuration**: constraints that another repo does not need -- or actively should not have -- get applied by your model.
  * **Writing "guaranteed execution" as an imperative sentence in a rule**: a rule is context only. The model can miss it in a multi-step task. Anything requiring a deterministic guarantee must go through a Hook.
  * **`paths` set too broadly**: using `**/*` as the paths value is the same as not scoping at all. The rule loads fully on every session startup, wasting the mechanism entirely.
  * **Confusing rules and Skills on trigger timing**: a rule is injected when Claude reads a matching file; a Skill is invoked when Claude judges it semantically relevant (see [04-4 Skills](/en/code-agent/customization/skills)). Writing "run ruff every time a Python file is edited" as a rule is correct; writing it as a Skill means it waits for an explicit invocation.
</Warning>

## Self-check

<Check>
  **The bar for passing this unit**

  1. Can you state the difference between rules and `CLAUDE.md`, and explain when to split something into rules?
  2. Can you write a correct `paths` frontmatter and use glob patterns to scope the loading range?
  3. Can you decide, given a requirement, whether a constraint belongs in `CLAUDE.md`, a standalone rule file, a Skill, or a Hook?
  4. Can you fill in the rule file mechanism for your primary tool on the five-tool comparison table?
</Check>

## Sources and further reading

Factual claims are grounded in official documentation; fast-changing items are annotated as of 2026-05.

<div className="references">
  * \[1] Anthropic, "How Claude remembers your project," code.claude.com, 2026. Available: [https://code.claude.com/docs/en/memory](https://code.claude.com/docs/en/memory) (as of 2026-06; includes `.claude/rules/`, `paths` frontmatter, symlinks, and auto memory chapters in full)

  * \[2] Anthropic, "Extend Claude with skills," code.claude.com, 2026. Available: [https://code.claude.com/docs/en/skills](https://code.claude.com/docs/en/skills) (as of 2026-06; includes the division of labor between Skills and `CLAUDE.md` / rules)

  * \[3] Cursor, "Rules," cursor.com, 2026. Available: [https://cursor.com/docs/context/rules](https://cursor.com/docs/context/rules) (as of 2026-06; `.mdc` and the `description` / `globs` / `alwaysApply` frontmatter fields)

  * \[4] GitHub Docs, "Adding custom instructions for GitHub Copilot," docs.github.com, 2026. Available: [https://docs.github.com/en/copilot/customizing-copilot/adding-custom-instructions-for-github-copilot](https://docs.github.com/en/copilot/customizing-copilot/adding-custom-instructions-for-github-copilot) (as of 2026-06; `.instructions.md` and the `applyTo` glob)

  * \[5] Agentic AI Foundation (Linux Foundation), "AGENTS.md," 2026. Available: [https://agents.md/](https://agents.md/) (as of 2026-06; cross-tool standard for project rule files)
</div>

* Settings layer model: [02-1 Config Layer Model](/en/code-agent/configuration/config-layer-model).
* `CLAUDE.md` and auto memory: [04-1 CLAUDE.md and Memory Files](/en/code-agent/customization/claude-md-memory).
* Skills (on-demand procedure invocation): [04-4 Skills](/en/code-agent/customization/skills).
* Hooks (deterministic guarantees): [04-6 Hooks](/en/code-agent/customization/hooks).
* Context engineering and rule loading costs: [01-4 Context Engineering](/en/code-agent/foundations/context-engineering).
