ClaudeMods
☰
KO
● 0 명 접속 중 · 조회 0 회
후원프로젝트 제출
GitHub 저장소 · 작성자 aliir74

rtl-text

fribidi를 사용해 트랜스크립트의 페르시아어, 아랍어, 히브리어를 형태 변환하고 재배열해 터미널에서 글자를 이어 쓰고 오른쪽에서 왼쪽으로, 오른쪽 정렬로 그리는 Claude Code 모드입니다.

번역 완료

이 mod 소개

rtl-text

터미널 트랜스크립트에서 페르시아어, 아랍어, 히브리어를 읽기 쉽게 만드는 Claude Code 모드입니다.

대부분의 터미널에는 UAX #9 bidi 처리와 아랍어 형태 변환이 없어 페르시아어가 거꾸로 나타나고 글자도 이어지지 않습니다. 이 모드는 트랜스크립트의 렌더링 이벤트를 연결하고 각 줄을 fribidi로 처리한 뒤 결과를 그립니다. 글자는 이어지고 순서는 오른쪽에서 왼쪽이며 RTL 문단은 오른쪽에 맞춰집니다.

Ghostty에서 같은 페르시아어 대화를 모드 적용 전후로 비교한 이미지

Claude Code 모드는 얼리 액세스 기능이며 기본적으로 꺼져 있습니다. 충분히 최신인 Claude Code에서 CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1을 설정하기 전에는 이 모드가 아무 작업도 하지 않습니다. Requirements를 참고하세요.

Claude Code가 출력하는 내용의 형태는 바꾸지만 프롬프트 상자에 입력하는 내용은 고칠 수 없습니다. Limits를 참고하세요.

Requirements

1. fribidi

brew install fribidi        # macOS
apt install fribidi         # Debian/Ubuntu

2. function-hooks 런타임을 포함할 만큼 최신인 Claude Code.

claude --version

모드는 얼리 액세스 기능입니다. 공개 변경 로그와 공식 문서에는 없으므로 알려진 "available from" 버전을 가리킬 수 없습니다. 알려진 사실은 2.1.260이 이 런타임을 포함한다고 보고된 가장 이른 빌드이며, 이 모드는 2.1.271부터 2.1.273까지에서 테스트되었다는 점입니다. 더 오래된 버전에서 모드가 아무것도 하지 않는다면 다른 문제를 디버깅하기 전에 업데이트하세요.

이 기능은 얼리 액세스이므로 플러그인 API가 릴리스 사이에 예고 없이 바뀔 수 있습니다. 따라서 Claude Code 업데이트로 이 모드가 깨져 다시 빌드할 때까지 작동하지 않을 수 있습니다. 이 경고는 생성된 타입 선언에 담긴 Anthropic 자체의 경고입니다.

3. Function hooks를 켭니다. 기능이 들어 있는 빌드에서도 환경 변수로 잠겨 있습니다. 이 변수를 설정하지 않으면 플러그인은 설치되고 로드되지만 조용히 아무것도 하지 않습니다. 이 모드가 작동하지 않는 것처럼 보이는 가장 흔한 이유입니다.

확실한 방법은 ~/.claude/settings.json에 설정하는 것입니다. 어떤 방식으로 시작하든 모든 세션에 적용됩니다.

{
  "env": {
    "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1"
  }
}

터미널에서만 Claude Code를 시작한다면 shell(~/.zshrc, ~/.bashrc)에서 export해도 됩니다.

export CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1

4. Arabic Presentation Forms 블록(U+FB50-U+FEFF)을 지원하는 고정폭 글꼴. 모드가 출력하는 범위이기 때문입니다. 고정폭이 중요합니다. 비례 글꼴의 페르시아어를 터미널 셀 격자에 억지로 넣으면 단어의 글자가 벌어지지만, 고정폭 글꼴은 이어진 형태가 셀 가장자리에서 만나도록 그립니다.

Vazir Code를 사용할 수 있습니다("Vazir Code Hack" 변형은 라틴 글리프에 Hack을 함께 사용합니다). 프로젝트는 중단되었지만 공개된 글꼴은 계속 쓸 수 있습니다. 중요한 빈틈이 하나 있습니다. U+FEF5-U+FEFC, 즉 8개의 lam-alef 합자 형태(سلام의 لا)가 없습니다. Vazirmatn에서 그 8개만 보충하세요. 이 글꼴도 설치해야 합니다. Ghostty에서는 다음과 같이 설정합니다.

font-family = "JetBrains Mono"
font-family = "Vazir Code Hack"
font-codepoint-map = U+FEF5-U+FEFC=Vazirmatn

Vazirmatn처럼 비례 글꼴을 일반 font-family 대체 글꼴로 추가하지 마세요. 라틴 글리프까지 이 글꼴이 차지해 영어 텍스트가 망가집니다.

Terminal support

모드 자체는 터미널에 종속되지 않습니다. Claude Code에 terminal surface를 요청할 뿐이며 어떤 터미널을 실행하는지 아는 코드는 없습니다. 도움이 될지 방해가 될지는 터미널이 자체 bidi 처리를 하는지에 달려 있습니다.

이 모드는 이미 시각적 순서로 재배열되고 이미 presentation forms로 변환된 텍스트를 출력합니다. 이 문자에는 여전히 강한 RTL bidi 클래스가 있으므로 터미널이 자체적으로 UAX #9를 실행하면 두 번째로 재배열되어 처음 상태로 돌아갑니다.

| Terminal | Use this mod? | | |---|---|---| | Ghostty | Yes — tested | bidi가 포함되어 있지 않습니다. #1442는 아직 open입니다 | | kitty | Expected yes | #2109는 2019년부터 open입니다 | | Alacritty | Expected yes | #663는 2017년부터 open입니다 | | foot | Expected yes | #756, 유지 관리자가 거절했습니다 | | Windows Terminal | Expected yes | #538는 2019년부터 open입니다 | | VS Code terminal (xterm.js) | Expected yes | vscode#271615 | | iTerm2 | 자체 RTL을 off로 할 때만 | 3.6+의 Settings → General → Experimental에 실험적 RTL이 있습니다. 기본값은 off입니다 | | WezTerm | bidi_enabled = false일 때만 | 이것이 기본값입니다 | | macOS Terminal.app | No | CoreText를 통한 네이티브 bidi가 있어 두 번 뒤집힙니다 | | GNOME Terminal / VTE | No | VTE 0.58부터 bidi를 지원합니다 | | Konsole | No | Bug 403729가 fixed로 해결되었습니다 | | mlterm | No | 대부분의 패키지처럼 --enable-fribidi로 빌드하면 bidi가 켜집니다 |

테스트된 것은 Ghostty 행뿐입니다. 나머지는 각 프로젝트의 자체 issue tracker에서 확인한 내용이므로 보장이 아니라 강한 사전 정보로 받아들이세요. 직접 사용해 보고 행을 고치는 PR을 보내 주시면 환영합니다.

터미널이 아래쪽 그룹에 속한다면 이 모드는 필요 없습니다. 논리적 텍스트에서 작업하고 composer도 처리할 수 있으므로 이 모드가 제공하는 것보다 렌더링이 이미 낫습니다.

Install

claude plugin marketplace add aliir74/claude-code-rtl
claude plugin install rtl-text@claude-code-rtl

설치하지 않고 clone에서 바로 실행할 수도 있습니다.

CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude --plugin-dir /path/to/claude-code-rtl

설치하면 사용자 범위의 enabledPlugins 항목이 직접 기록되어 모든 프로젝트에서 켜집니다. 이후 업데이트:

claude plugin update rtl-text

요구 사항 3을 기억하세요. CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1이 없으면 플러그인은 정상 설치된 뒤 아무것도 하지 않습니다.

What it covers

| Component | Shaped | |---|---| | AssistantMessage | markdown으로 yes | | UserMessage | markdown으로 yes. 배경 밴드를 유지할 수 있도록 엔진에 돌려줍니다 | | CommandOutput | 일반 텍스트로 yes | | ToolResult (Bash only) | 일반 텍스트로 yes |

Limits

composer는 포함되지 않습니다. 프롬프트 상자에 페르시아어를 입력하는 문제는 그대로이며 어떤 모드도 고칠 수 없습니다. 입력은 RenderComponent가 아니고, ui.input은 렌더링 hook이 직접 그린 Input 요소에서만 발생하며 Claude Code 자체 composer에서는 발생하지 않기 때문입니다. 실제 bidi를 얻는 터미널만 이 문제를 해결할 수 있습니다. Ghostty에서는 PR #14142가 그 변경이지만 2026년 9월 기준으로 아직 merge되지 않았습니다.

형태가 변환된 텍스트를 복사하면 presentation forms가 복사됩니다. 화면에 보이는 것이 그대로 yank되므로 트랜스크립트에서 복사한 텍스트는 시각적 순서가 되고 논리적 순서의 편집기에 깔끔하게 다시 붙여 넣을 수 없습니다.

일부러 건드리지 않는 것이 2가지 있습니다. 코드 블록, 인라인 `code` 범위, URL은 수정 없이 통과시키므로 명령의 순서가 바뀌지 않습니다. Bash 결과가 너무 커서 파일에 저장된 경우에도 엔진에 맡깁니다. stdout을 다시 그리면 잘린 화면을 전체 결과인 것처럼 보여 주기 때문입니다.

Options

플러그인의 설정에서 지정합니다. 모두 선택 사항이고 코드에서 범위가 제한되므로 잘못된 값도 로드를 실패시키지 않고 성능을 낮추는 쪽으로 처리됩니다.

| Option | Default | Meaning | |---|---|---| | alignment | auto | auto는 자체 기본 방향이 RTL인 문단만 오른쪽 정렬합니다. left는 패딩 없이 형태를 바꾸고, right는 항상 오른쪽 정렬합니다 | | fribidiPath | fribidi | 바이너리 경로 | | margin | 4 | 줄바꿈 전에 viewport.columns에서 남겨 두는 셀 수입니다. 1 이상으로 유지하세요. 셀 측정이 1만큼 어긋날 때 wrap: 'truncate-end'가 오른쪽 정렬 줄의 시작을 잘라 내지 않게 하는 여유입니다 | | cacheSize | 256 | LRU에 보관하는 형태 변환 메시지 수 | | timeoutMs | 2000 | 행이 엔진 자체 그리기로 대체되기 전에 fribidi 호출에 허용되는 시간 | | replyBullet | ⏺ | 답변 시작 줄의 표시입니다. 자체 트리를 그리면 표시를 포함한 엔진의 전체 행을 대체하므로 모드가 다시 그립니다. RTL에서는 문장이 시작되는 오른쪽 가장자리에 놓이며 빈 문자열이면 표시하지 않습니다 |

Troubleshooting

아무것도 바뀌지 않습니다. 모드는 의도적으로 조용히 실패합니다. 모든 hook은 트랜스크립트를 망가뜨리는 대신 엔진 자체 행으로 대체됩니다. 다음 목록을 순서대로 확인하세요.

fribidi --version                              # is the binary there?
grep FUNCTION_HOOKS ~/.claude/settings.json    # is the gate set?
claude plugin list                             # is rtl-text installed and enabled?

shell에서 변수를 export하고 settings.json에 넣지 않았다면 echo $CLAUDE_CODE_ENABLE_FUNCTION_HOOKS로 확인하세요. settings.json 항목은 여기에 나타나지 않습니다. Claude Code 프로세스 안에서 설정되며 shell에 설정되는 것이 아니기 때문입니다.

fribidi가 특이한 위치에 설치되어 있다면 fribidiPath를 절대 경로로 설정하세요.

트랜스크립트에 no runnable fribidi after 3 tries가 표시됩니다. 모드는 첫 렌더링 때 바이너리를 탐색하고 각 후보가 실패한 이유를 출력합니다. 메시지에는 실제 이유가 나오며 대개 경로 문제입니다.

글자는 이어지지만 간격이 넓습니다. 글꼴 문제이지 모드 문제가 아닙니다. 위의 요구 사항 4를 보세요. 거의 확실히 비례 글꼴로 렌더링하고 있습니다.

페르시아어가 뒤집힙니다. 터미널에 자체 bidi가 있어 모드의 작업을 되돌리고 있을 수 있습니다. 터미널 지원 표를 확인하세요.

How it works

text -> hasRtl? -> split blocks -> split markdown prefix -> protect code/URLs
     -> WRAP in logical order -> one fribidi call per direction -> restore -> pad -> Text rows

순서가 설계의 전부입니다. 줄바꿈은 뷰포트 너비에 맞춰 논리적 순서로 일어나고 완성된 줄만 형태가 바뀝니다. 먼저 형태를 바꾼 뒤 시각적 결과를 줄바꿈하면 문단의 첫 단어가 마지막 줄로 가는데, harness/transform-real.check.ts가 잡아내는 버그입니다.

알아 둘 만한 결정이 2가지 더 있습니다.

  • fribidi는 대신 줄바꿈하지 않습니다. 자체 --width 줄바꿈은 단어를 인식하지 않고 token 중간에서 나누므로 모든 호출에 --nobreak을 전달하고 줄바꿈은 우리가 처리합니다.
  • 패딩도 우리가 처리합니다. fribidi의 --width 패딩은 표시 셀 기준으로 정확하지만 줄마다 auto 정렬을 표현할 수 없어서 cellWidth가 처리합니다. 그래서 cellWidth가 중요한 기반이 되며 harness는 독립적인 기준으로 fribidi 자체 패딩과 대조합니다.

기본 방향마다 fribidi 하위 프로세스 하나가 한 메시지 전체를 처리하며 줄마다 따로 실행하지 않습니다. 결과는 (markdown, width, text)를 키로 캐시됩니다. 렌더링 hook이 다시 그리거나 크기를 조정할 때마다 재실행되기 때문입니다.

Development

.claude-plugin/plugin.json   manifest and userConfig
.claude-plugin/marketplace.json
hooks/                       the mod; no npm dependencies, no Node imports
  register.ts                the four ui.render hooks and the engine adapter
  transform.ts               the pipeline
  segment.ts                 blocks, markdown prefixes, inline protection, tables
  wrap.ts  cell-width.ts  align.ts  lru-cache.ts  rtl-detect.ts  fribidi-args.ts
  render-tree.ts             lines -> the surface's Box/Text/Code constructors
tests/                       run by `claude plugin test`
harness/*.check.ts           run by tsx; the only tests that touch the real binary
.claude/types/               generated by /plugin-types, do not hand-edit
docs/research/               measured findings this mod was built from

서로를 대신하지 않으므로 검사는 4가지입니다.

CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude plugin test .     # hooks + pure logic, fribidi mocked
npx --yes tsx --test harness/*.check.ts                      # the real fribidi binary
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude plugin validate .  # structure: hooks, matchers, $ calls
npx --yes -p t

설치

먼저 작성자의 README에서 marketplace와 플러그인 이름을 확인하세요. 저장소 구조에 따라 명령어가 달라질 수 있습니다.

claude plugin marketplace add aliir74/claude-code-rtl
claude plugin install rtl-text
원문 / README

rtl-text

A Claude Code mod that makes Persian, Arabic and Hebrew readable in the terminal transcript.

Most terminals have no UAX #9 bidi and no Arabic shaping, so Persian arrives reversed and with its letters unjoined. This mod hooks the transcript's render events, runs each line through fribidi, and draws the result: letters joined, order right-to-left, RTL paragraphs flush right.

The same Persian exchange in Ghostty, before and after the mod

Claude Code mods are early access and off by default. This one does nothing at all until you set CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 on a recent enough Claude Code. See Requirements.

It shapes what Claude Code prints. It cannot fix what you type into the prompt box; see Limits.

Requirements

1. fribidi

brew install fribidi        # macOS
apt install fribidi         # Debian/Ubuntu

2. A Claude Code new enough to carry the function-hooks runtime.

claude --version

Mods are an early-access feature. They are not in the public changelog and not in the official docs, so there is no published "available from" version to point at. What is known: 2.1.260 is the earliest build reported to carry the runtime, and this mod is tested on 2.1.271 through 2.1.273. If you are on something older and the mod does nothing, update before debugging anything else.

Because the feature is early access, the plugin API can change between releases without notice, so a Claude Code update may break this mod until it is rebuilt. That warning is Anthropic's own, from the generated type declarations.

3. Function hooks switched on. The feature is gated behind an environment variable even on a build that has it. Without it the plugin installs, loads and silently does nothing, which is the single most common reason this mod appears not to work.

The durable way is ~/.claude/settings.json, which applies to every session however you start it:

{
  "env": {
    "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1"
  }
}

Or export it in your shell (~/.zshrc, ~/.bashrc) if you only ever launch Claude Code from a terminal:

export CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1

4. A monospace font covering the Arabic Presentation Forms block (U+FB50-U+FEFF), which is what this mod emits. Monospace matters: a proportional Persian face forced into a terminal's cell grid pulls the letters of a word apart, where a monospace face has its joined forms drawn to meet at the cell edges.

Vazir Code works (the "Vazir Code Hack" variant pairs it with Hack for the Latin glyphs). Note the project is discontinued, though the released fonts are fine. It has one gap that matters: no U+FEF5-U+FEFC, the eight lam-alef ligature forms (the لا in سلام). Fill just those from Vazirmatn, which you will need installed as well. In Ghostty:

font-family = "JetBrains Mono"
font-family = "Vazir Code Hack"
font-codepoint-map = U+FEF5-U+FEFC=Vazirmatn

Do not add a proportional face like Vazirmatn as a plain font-family fallback. It wins the Latin glyphs too and spoils your English text.

Terminal support

The mod itself is terminal-agnostic: it asks Claude Code for the terminal surface and nothing in it knows which terminal you run. What decides whether it helps or hurts is whether your terminal does its own bidi.

This mod emits text already reordered into visual order and already converted to presentation forms. Those characters still carry a strong RTL bidi class, so a terminal that runs its own UAX #9 pass will reorder them a second time and put you back where you started.

| Terminal | Use this mod? | | |---|---|---| | Ghostty | Yes — tested | No bidi shipped; #1442 open | | kitty | Expected yes | #2109 open since 2019 | | Alacritty | Expected yes | #663 open since 2017 | | foot | Expected yes | #756, declined by the maintainer | | Windows Terminal | Expected yes | #538 open since 2019 | | VS Code terminal (xterm.js) | Expected yes | vscode#271615 | | iTerm2 | Only with its own RTL off | 3.6+ has experimental RTL under Settings → General → Experimental; off by default | | WezTerm | Only with bidi_enabled = false | That is the default | | macOS Terminal.app | No | Native bidi via CoreText; would double-reverse | | GNOME Terminal / VTE | No | Bidi since VTE 0.58 | | Konsole | No | Bug 403729 resolved fixed | | mlterm | No | Bidi when built --enable-fribidi, as most packages are |

Only the Ghostty row is tested. Everything else is read off each project's own issue tracker, so treat it as a strong prior rather than a promise. If you try one, a PR correcting the row is welcome.

If your terminal is in the bottom group, you do not need this mod. Its rendering is already better than what this mod can offer, since it works on logical text and can handle the composer too.

Install

claude plugin marketplace add aliir74/claude-code-rtl
claude plugin install rtl-text@claude-code-rtl

Or run it straight from a clone, without installing:

CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude --plugin-dir /path/to/claude-code-rtl

Installing writes the enabledPlugins entry itself, at user scope, so it is on in every project. Later updates:

claude plugin update rtl-text

Remember requirement 3: without CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 the plugin installs successfully and then does nothing.

What it covers

| Component | Shaped | |---|---| | AssistantMessage | yes, as markdown | | UserMessage | yes, as markdown, handed back to the engine so it keeps its background band | | CommandOutput | yes, as plain text | | ToolResult (Bash only) | yes, as plain text |

Limits

The composer is not covered. Typing Persian into the prompt box is still broken, and no mod can fix it: the input is not a RenderComponent, and ui.input fires only for Input elements a render hook itself drew, never for Claude Code's own composer. Only your terminal gaining real bidi fixes that. For Ghostty that is PR #14142, not merged as of September 2026.

Copying shaped text gives you presentation forms. What is on screen is what gets yanked, so text copied out of the transcript is in visual order and will not paste cleanly back into a logical-order editor.

Two things it deliberately leaves alone. A code block, an inline `code` span and a URL are passed through untouched, so nothing reorders your commands. A Bash result whose output was too large and got persisted to a file is left to the engine, because redrawing stdout would present a truncated view as if it were the whole thing.

Options

Set them in the plugin's config. Every one is optional and clamped in code, so a bad value degrades rather than failing the load.

| Option | Default | Meaning | |---|---|---| | alignment | auto | auto right-aligns only paragraphs whose own base direction is RTL; left shapes without padding; right always right-aligns | | fribidiPath | fribidi | Path to the binary | | margin | 4 | Cells held back from viewport.columns before wrapping. Keep it at 1 or more: it is the slack that stops wrap: 'truncate-end' clipping the start of a right-aligned line if the cell measure is ever off by one | | cacheSize | 256 | Shaped messages kept in the LRU | | timeoutMs | 2000 | How long a fribidi call may take before the row falls back to the engine's own drawing | | replyBullet | ⏺ | Marker on the opening line of a reply. Drawing our own tree replaces the engine's whole row, marker included, so the mod redraws it, on the right edge for RTL where the sentence starts; an empty string leaves it off |

Troubleshooting

Nothing changes at all. The mod is failing silently by design: every hook falls back to the engine's own row rather than breaking your transcript. Work down this list.

fribidi --version                              # is the binary there?
grep FUNCTION_HOOKS ~/.claude/settings.json    # is the gate set?
claude plugin list                             # is rtl-text installed and enabled?

If you exported the variable in your shell instead of putting it in settings.json, check it with echo $CLAUDE_CODE_ENABLE_FUNCTION_HOOKS. A settings.json entry will not show up there: it is set inside the Claude Code process, not in your shell.

If fribidi is installed somewhere unusual, set fribidiPath to its absolute path.

A no runnable fribidi after 3 tries line in the transcript. The mod probes for the binary on the first render and prints why each candidate failed. The message names the real reason, which is usually a path problem.

Letters are joined but gappy. That is the font, not the mod. See requirement 4 above: you are almost certainly rendering with a proportional face.

Persian text is reversed. Your terminal probably has its own bidi, and it is undoing the mod's work. Check the terminal support table.

How it works

text -> hasRtl? -> split blocks -> split markdown prefix -> protect code/URLs
     -> WRAP in logical order -> one fribidi call per direction -> restore -> pad -> Text rows

The ordering is the whole design. Wrapping happens in logical order against the viewport width, and only the finished lines are shaped. Shaping first and wrapping the visual result puts the paragraph's opening words on the last line, which is the bug harness/transform-real.check.ts exists to catch.

Two other decisions worth knowing:

  • fribidi never breaks lines for us. Its own --width breaking is not word-aware and splits words mid-token, so every call passes --nobreak and the wrapping is ours.
  • Padding is ours too. fribidi's --width padding is display-cell accurate, but it cannot express a per-line auto alignment, so cellWidth does it. That makes cellWidth load-bearing, which is why the harness checks it against fribidi's own padding as an independent oracle.

One fribidi subprocess handles a whole message per base direction, never one per line, and the result is cached by (markdown, width, text) because render hooks re-run on every redraw and resize.

Development

.claude-plugin/plugin.json   manifest and userConfig
.claude-plugin/marketplace.json
hooks/                       the mod; no npm dependencies, no Node imports
  register.ts                the four ui.render hooks and the engine adapter
  transform.ts               the pipeline
  segment.ts                 blocks, markdown prefixes, inline protection, tables
  wrap.ts  cell-width.ts  align.ts  lru-cache.ts  rtl-detect.ts  fribidi-args.ts
  render-tree.ts             lines -> the surface's Box/Text/Code constructors
tests/                       run by `claude plugin test`
harness/*.check.ts           run by tsx; the only tests that touch the real binary
.claude/types/               generated by /plugin-types, do not hand-edit
docs/research/               measured findings this mod was built from

Four checks, because none covers another:

CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude plugin test .     # hooks + pure logic, fribidi mocked
npx --yes tsx --test harness/*.check.ts                      # the real fribidi binary
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude plugin validate .  # structure: hooks, matchers, $ calls
npx --yes -p typescript@latest tsc -p tsconfig.json           # types

The hooks environment has no fs, network or process, so the real-binary tests cannot live under claude plugin test. They are named *.check.ts rather than *.test.ts because that runner globs the whole plugin directory and would refuse a module importing node:child_process.

Notes for anyone editing this

  • Elements are never object literals. They come from $.ui.resolve(e). A { type: 'Box' } literal typechecks fine and is then refused at runtime, and the engine quietly redraws its own row, which looks exactly like the mod not being installed.
  • $ may only be passed to a function declared at the top of the file. claude plugin validate enforces this so it can report which $ calls a plugin makes. That is why the helpers in register.ts are top-level functions taking a Context rather than closures.
  • No raw NUL or private-use characters in source. Use String.fromCharCode. A literal escape can land as a real control byte, which turns the file binary and makes grep silently miss it.
  • userConfig entries need a title. The manifest schema rejects them otherwise.
  • Regenerate the type contract after a Claude Code update: claude -p '/plugin-types'.
  • Bump the version in BOTH plugin.json and the marketplace.json entry, in sync. The plugin cache is keyed on that string, so an unbumped release does not reach anyone who has already installed it, and a marketplace entry version overrides the manifest's if they differ.

Status

Working, confirmed in Ghostty 1.3.2 on Claude Code 2.1.273. Persian renders joined, right-to-left and flush right, Latin runs inside a Persian sentence keep their own direction, and code blocks pass through.

License

MIT

비슷한 프로젝트