aliir74/claude-code-rtl

端末のトランスクリプト内でペルシャ語、アラビア語、ヘブライ語を fribidi で整形・並べ替えし、字形をつなげて右から左、右寄せで描画する Claude Code mod。
aliir74/claude-code-rtl

端末のトランスクリプトでペルシャ語、アラビア語、ヘブライ語を読みやすくする Claude Code mod です。
ほとんどの端末には UAX #9 の bidi 処理もアラビア語の字形整形もないため、ペルシャ語は逆向きに表示され、文字もつながりません。この mod はトランスクリプトの描画イベントをフックし、各行を fribidi に通して結果を描画します。字形はつながり、順序は右から左、RTL の段落は右端に揃います。

Claude Code mod は早期アクセスで、デフォルトでは無効です。 十分に新しい Claude Code で
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1を設定するまで、この mod は何もしません。 Requirements を参照してください。
Claude Code が出力する文字は整形しますが、プロンプトボックスに入力した文字は直せません。Limits を参照してください。
1. fribidi
brew install fribidi # macOS
apt install fribidi # Debian/Ubuntu
2. function-hooks ランタイムを含む十分に新しい Claude Code。
claude --version
mod は早期アクセス機能です。公開の変更履歴にも公式ドキュメントにも載っていないため、指し示せる公開された「available from」バージョンはありません。分かっていることは、2.1.260 がこのランタイムを含むと報告された最初期のビルドであり、この mod は 2.1.271 から 2.1.273 でテストされていることです。古いバージョンで mod が何もしない場合は、ほかの調査をする前に更新してください。
この機能は早期アクセスなので、プラグイン API がリリース間で予告なく変わる可能性があります。そのため Claude Code の更新でこの mod が壊れ、再ビルドされるまで動かなくなることがあります。この警告は、生成された型宣言にある Anthropic 自身の警告です。
3. Function hooks を有効にすること。 機能を含むビルドでも、環境変数でゲートされています。設定しないとプラグインはインストールされ、読み込まれますが、静かに何もしません。これは、この mod が動かないように見える最もよくある理由です。
確実な方法は ~/.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)をカバーする等幅フォント。 これは mod が出力する範囲です。等幅であることが重要です。比例フォントのペルシャ文字を端末のセルグリッドに押し込むと、単語の文字がばらけます。等幅フォントなら、つながった字形がセルの端で接するように描画されます。
Vazir Code が使えます(「Vazir Code Hack」バリアントは Latin の字形に Hack を組み合わせます)。プロジェクトは終了している点に注意してください。ただし公開済みのフォントは問題なく使えます。重要な欠落が 1 つあります。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 フォールバックとして追加しないでください。Latin の字形までそれが選ばれ、英語の表示が崩れます。
mod 自体は端末に依存しません。Claude Code に terminal surface を要求するだけで、どの端末を使っているかを知るコードはありません。役に立つか、逆効果になるかを決めるのは、端末が独自に bidi を処理するかどうかです。
この mod は、すでに視覚的な順序に並べ替えられ、すでに presentation forms に変換された文字を出力します。それらの文字にも強い RTL の bidi class が残るため、端末が独自に 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 があれば歓迎します。
端末が下のグループにあるなら、この mod は必要ありません。論理順序の文字列を扱い、composer も処理できるため、描画はこの mod より優れています。
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 がないと、プラグインは正常にインストールされても何もしません。
| Component | Shaped |
|---|---|
| AssistantMessage | markdown として yes |
| UserMessage | markdown として yes。背景の帯を維持できるようエンジンへ返します |
| CommandOutput | プレーンテキストとして yes |
| ToolResult (Bash only) | プレーンテキストとして yes |
composer は対象外です。 プロンプトボックスにペルシャ語を入力しても壊れたままで、mod では直せません。入力は RenderComponent ではなく、ui.input はレンダリング hook 自身が描いた Input 要素に対してだけ発火し、Claude Code 自身の composer には発火しないためです。これを直せるのは、端末が本物の bidi を獲得する場合だけです。Ghostty では PR #14142 がそれに当たりますが、2026 年 9 月時点で未マージです。
整形済みの文字をコピーすると presentation forms になります。 画面に表示されているものがそのまま yank されるため、トランスクリプトからコピーした文字は視覚順序になり、論理順序のエディターへきれいに貼り戻せません。
意図的にそのままにするものが 2 つあります。コードブロック、インラインの `code` スパン、URL は変更せずに通すので、コマンドの順序は変わりません。Bash の結果が大きすぎてファイルに保存された場合もエンジンに任せます。stdout を描き直すと、切り詰められた表示を全体であるかのように見せてしまうためです。
プラグインの設定で指定します。すべて任意で、コード内で範囲も制限されるため、悪い値でも読み込みに失敗せず劣化します。
| 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 | ⏺ | 返信の開始行に付けるマーカー。独自のツリーを描くと、マーカーを含むエンジンの行全体を置き換えるため、mod が再描画します。RTL では文の始まる右端に置かれ、空文字列なら表示しません |
何も変わらない。 mod は意図的に静かに失敗します。すべての 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 と表示される。 最初の描画でバイナリを調べ、候補ごとに失敗した理由を表示します。メッセージには実際の理由が示され、通常はパスの問題です。
字形はつながるが隙間がある。 フォントの問題で、mod の問題ではありません。上の要件 4 を確認してください。ほぼ確実に比例フォントで描画しています。
ペルシャ語が逆になる。 端末に独自の bidi があり、mod の仕事を取り消している可能性があります。端末サポート表を確認してください。
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 つあります:
--width による改行は単語を認識せず、token の途中で分割するため、すべての呼び出しに --nobreak を渡し、折り返しはこちらで行います。--width によるパディングは表示セルに対して正確ですが、行ごとの auto 配置を表現できないため cellWidth が処理します。そのため cellWidth は重要な土台であり、harness は独立した基準として fribidi 自身のパディングと照合します。基準方向ごとに 1 つの fribidi サブプロセスでメッセージ全体を処理し、行ごとには起動しません。結果は (markdown, width, text) をキーにキャッシュします。描画 hook は再描画やリサイズのたびに再実行されるためです。
.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
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.

Claude Code mods are early access and off by default. This one does nothing at all until you set
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1on 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.
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.
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.
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.
| 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 |
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.
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 |
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.
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:
--width breaking is not word-aware and splits
words mid-token, so every call passes --nobreak and the wrapping is ours.--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.
.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.
$.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.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.claude -p '/plugin-types'.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.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.
MIT