<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Erkan DOGAN</title>
    <description>The latest articles on DEV Community by Erkan DOGAN (@erkandogan).</description>
    <link>https://dev.to/erkandogan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3878898%2F23a5c83e-dd51-4d50-b525-dbb42d054e55.png</url>
      <title>DEV Community: Erkan DOGAN</title>
      <link>https://dev.to/erkandogan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/erkandogan"/>
    <language>en</language>
    <item>
      <title>Playing DOOM in Claude Code's Statusline (and Fighting Its Renderer to Keep It There)</title>
      <dc:creator>Erkan DOGAN</dc:creator>
      <pubDate>Wed, 22 Apr 2026 08:02:02 +0000</pubDate>
      <link>https://dev.to/erkandogan/playing-doom-in-claude-codes-statusline-and-fighting-its-renderer-to-keep-it-there-3n7k</link>
      <guid>https://dev.to/erkandogan/playing-doom-in-claude-codes-statusline-and-fighting-its-renderer-to-keep-it-there-3n7k</guid>
      <description>&lt;h1&gt;
  
  
  Playing DOOM in Claude Code's Statusline
&lt;/h1&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/zuUPhp0G2sI"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I made DOOM run inside Claude Code.&lt;/strong&gt; Real DOOM — the 1993 id Software engine, rendering live in the bottom of the Claude Code window, right there in the status-bar area. You move the player by typing &lt;code&gt;w&lt;/code&gt;/&lt;code&gt;a&lt;/code&gt;/&lt;code&gt;s&lt;/code&gt;/&lt;code&gt;d&lt;/code&gt;/&lt;code&gt;f&lt;/code&gt; into the chat prompt, the same way you'd type a message to Claude. Anything that isn't a recognized key falls through to Claude as a normal chat turn. And Claude itself can play the game, because I exposed the inputs and game state through an MCP server.&lt;/p&gt;

&lt;p&gt;No patched binary. No VS Code extension. No electron window. Just four of Claude Code's own extension surfaces — statusline, UserPromptSubmit hook, MCP server, slash command — composed in a way that wasn't obviously intended but turns out to work.&lt;/p&gt;

&lt;p&gt;The gameplay architecture took a few hundred lines of code. The &lt;em&gt;rest of this post&lt;/em&gt; is about the ten hours of debugging that came after, because Claude Code's Ink-based terminal renderer really, really did not want to hold a live DOOM frame steady in its statusline.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it actually works
&lt;/h2&gt;

&lt;p&gt;A background daemon runs the &lt;a href="https://github.com/ozkl/doomgeneric" rel="noopener noreferrer"&gt;doomgeneric&lt;/a&gt; DOOM engine and writes each rendered frame as 24-bit ANSI to &lt;code&gt;/tmp/doom-in-claude/frame.ansi&lt;/code&gt;. Claude Code's &lt;strong&gt;statusline&lt;/strong&gt; &lt;code&gt;cat&lt;/code&gt;s that file on every refresh. A &lt;strong&gt;UserPromptSubmit hook&lt;/strong&gt; interprets &lt;code&gt;w&lt;/code&gt;/&lt;code&gt;a&lt;/code&gt;/&lt;code&gt;s&lt;/code&gt;/&lt;code&gt;d&lt;/code&gt;/&lt;code&gt;f&lt;/code&gt; typed in the prompt as DOOM keys. An &lt;strong&gt;MCP server&lt;/strong&gt; exposes &lt;code&gt;doom_look&lt;/code&gt; / &lt;code&gt;doom_move&lt;/code&gt; / &lt;code&gt;doom_state&lt;/code&gt; so Claude itself can play.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;daemon (doomgeneric)  →  frame.ansi / frame.png / state.json
         ↑                                  ↓
    input.fifo                           statusline (display)
         ↑                                  ↓
    UserPromptSubmit hook  ←   you type `wwwf` in the prompt
                                            ↓
                                      MCP server (Claude's tools)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four existing Claude Code extension points, a 900-line C wrapper, and a tiny statusline shell script. That's the feature.&lt;/p&gt;

&lt;p&gt;The rest of this post is about what happened when I tried to make it &lt;em&gt;render cleanly&lt;/em&gt;. None of the complexity is in the gameplay pipeline. All of it is in getting the multi-line 24-bit-ANSI stream past Claude Code's Ink-based TUI renderer without visual drift.&lt;/p&gt;

&lt;p&gt;Repo: &lt;strong&gt;&lt;a href="https://github.com/erkandogan/doom-in-claude-code" rel="noopener noreferrer"&gt;https://github.com/erkandogan/doom-in-claude-code&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;DOOM starts. First frame — perfect. You walk forward. Second frame — perfect. You walk a few more, start seeing enemies, fire the pistol. Around frame 20 or 30, walls start showing wrong colors. By frame 50, the image is scrambled noise. Quitting &lt;code&gt;/doom stop&lt;/code&gt; doesn't fix it. Only a full Claude Code restart clears it.&lt;/p&gt;

&lt;p&gt;My first guess was the daemon — maybe doomgeneric's framebuffer was being overwritten mid-read. No: the frame file was atomic-rename-written, byte counts were consistent when I inspected them after-the-fact. Whatever was corrupting the rendering lived &lt;strong&gt;after&lt;/strong&gt; we wrote to disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ink keeps its own mental model of the screen
&lt;/h2&gt;

&lt;p&gt;Claude Code's TUI is built on &lt;a href="https://github.com/vadimdemedes/ink" rel="noopener noreferrer"&gt;Ink&lt;/a&gt; — React for terminals. Ink maintains a virtual-DOM-like cell grid, and when the statusline updates, Ink doesn't re-emit the whole thing — it computes a diff from its cached previous state and emits only the changed cells. This is fast and flicker-free for text, and it's the cause of our drift.&lt;/p&gt;

&lt;p&gt;A live DOOM frame has ~3,000 cells (120 × 28), every one emitting a 24-bit foreground/background SGR on each refresh. That's &lt;strong&gt;~6,000 SGR escape sequences and ~129 KB of bytes per frame&lt;/strong&gt;, refreshed multiple times per second. Ink's diff engine is perfectly competent at this volume of text but occasionally miscounts cell widths, and &lt;strong&gt;the miscounts compound&lt;/strong&gt;. Each frame, Ink's mental model drifts a little further from the actual terminal. After ~30 frames, cells its model thinks are at column 42 are physically at column 43. Colors get painted on wrong glyphs. Your image becomes noise.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hypothesis 1: Stabilize the bytes
&lt;/h2&gt;

&lt;p&gt;First theory: maybe Ink's miscounting comes from variable byte-structure between frames. Each cell emits something like &lt;code&gt;\x1b[38;2;R;G;B;48;2;R;G;Bm&amp;lt;glyph&amp;gt;\x1b[0m&lt;/code&gt; — where R, G, B are 1 to 3 digits. A red value of 5 is 1 byte, 255 is 3 bytes. So even in a static scene where the glyphs don't change, color values drifting by tiny dithering amounts produced different byte offsets between frames.&lt;/p&gt;

&lt;p&gt;Fix: a perl normalizer between chafa and the atomic-rename write, which:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pads every RGB triple to 3 digits: &lt;code&gt;38;2;255;60;60m&lt;/code&gt; → &lt;code&gt;38;2;255;060;060m&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Replaces 1-byte ASCII spaces (chafa emits these for blank cells) with 3-byte &lt;code&gt;U+2800&lt;/code&gt; Braille blanks — identical visual, stable byte count&lt;/li&gt;
&lt;li&gt;Strips DEC private modes (cursor hide/show) that Ink doesn't track&lt;/li&gt;
&lt;li&gt;Collapses duplicate &lt;code&gt;\x1b[0m&lt;/code&gt; resets&lt;/li&gt;
&lt;li&gt;Ensures each line ends with exactly one &lt;code&gt;\x1b[0m\n&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Result: &lt;strong&gt;every byte position in the frame is identical frame-to-frame except the RGB digit values themselves&lt;/strong&gt;. 97% byte-identical between successive frames. Drift &lt;em&gt;should&lt;/em&gt; be impossible.&lt;/p&gt;

&lt;p&gt;It still drifted. Less, but it still drifted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hypothesis 2: I was fixing the wrong render path
&lt;/h2&gt;

&lt;p&gt;Embarrassingly, I had been running the sextant renderer (our fallback native C path) the whole time, not chafa. My perl normalizer only ran in the chafa path. Half a day of byte-stability debugging was invisibly routed around.&lt;/p&gt;

&lt;p&gt;The comment in my own code had warned me: Unicode 13 sextant glyphs (U+1FB00..U+1FB3B) aren't recognized by most &lt;code&gt;string-width&lt;/code&gt; libraries, so Ink can't measure their width, so every sextant-heavy cell nudges Ink's column tracker a fractional cell. The existing sextant renderer was structurally incompatible with Ink regardless of byte stability.&lt;/p&gt;

&lt;p&gt;Fix: change default render mode to &lt;code&gt;chafa-if-available-else-quadrant&lt;/code&gt; (quadrant uses only U+2580–U+259F block glyphs, which every string-width library handles correctly).&lt;/p&gt;

&lt;h2&gt;
  
  
  Hypothesis 3: The HUD line I wasn't stabilizing
&lt;/h2&gt;

&lt;p&gt;The top line of the DOOM statusline is a rich HUD: &lt;code&gt;DOOM E1M1  HP 100  ARM 0  PISTOL 50/200  KILLS 0  ITEMS 0  SECRETS 0&lt;/code&gt;. Every field used &lt;code&gt;%d&lt;/code&gt; — so when HP dropped from 100 to 65, the line's byte count shrank by 1. Weapon names were 4–8 chars, so switching from FIST to CHAINGUN shifted bytes. And there were TWO different format strings depending on whether the weapon had ammo.&lt;/p&gt;

&lt;p&gt;Fix: one unified format string, every field padded to its maximum width:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="s"&gt;"%sHP %3d%s  %sARM %3d%s  %s%-8s%s %s%s%s  %sKILLS %4d  ITEMS %4d  SECRETS %3d"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HUD went from variable byte count to a rock-solid 244 bytes in every possible game state. Only then was the &lt;em&gt;whole&lt;/em&gt; frame byte-stable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The regex bug that only fired on palette-2
&lt;/h2&gt;

&lt;p&gt;With 256-color chafa output, one SGR in every frame came out 3 bytes longer than its neighbors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;\x1b[38;&lt;/span&gt;5&lt;span class="p"&gt;;&lt;/span&gt;2&lt;span class="p"&gt;;&lt;/span&gt;48&lt;span class="p"&gt;;&lt;/span&gt;5&lt;span class="p"&gt;;&lt;/span&gt;22m    &lt;span class="o"&gt;(&lt;/span&gt;raw chafa&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;\x1b[38;&lt;/span&gt;5&lt;span class="p"&gt;;&lt;/span&gt;002&lt;span class="p"&gt;;&lt;/span&gt;048&lt;span class="p"&gt;;&lt;/span&gt;005&lt;span class="p"&gt;;&lt;/span&gt;022m   &lt;span class="o"&gt;(&lt;/span&gt;after my padding — 048 is wrong!&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My RGB-padding regex was &lt;code&gt;;2;(\d+);(\d+);(\d+)&lt;/code&gt; — which happens to match &lt;code&gt;;2;48;5;22&lt;/code&gt; inside a 256-palette SGR where the fg palette index is &lt;code&gt;2&lt;/code&gt;. The regex thought &lt;code&gt;48&lt;/code&gt; was a red value and padded it to &lt;code&gt;048&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Fix: anchor the regex with &lt;code&gt;(?&amp;lt;![0-9])(38|48);2;&lt;/code&gt; so padding only fires when &lt;code&gt;38&lt;/code&gt; or &lt;code&gt;48&lt;/code&gt; is at the start of an SGR parameter list. Classic "regex fires on subsequence inside another structure" bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hypothesis 4: Maybe all this byte work is missing the point
&lt;/h2&gt;

&lt;p&gt;10 action frames captured during heavy play. All exactly 81,282 bytes. 97% byte-identical. Drift still visible under damage flashes.&lt;/p&gt;

&lt;p&gt;That was the moment I actually read Claude Code's statusline docs. Buried in the troubleshooting section:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Complex escape sequences (ANSI colors, OSC 8 links) can occasionally cause garbled output if they overlap with other UI updates.&lt;/p&gt;

&lt;p&gt;Multi-line status lines with escape codes are more prone to rendering issues than single-line plain text.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude Code's own docs warn about exactly this. The drift isn't a solvable bug in our bytes; it's a &lt;strong&gt;known architectural limitation&lt;/strong&gt; of multi-line ANSI statuslines.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stress reduction, then real fixes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Smaller frames
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Dropped frame rate from 6.7 Hz → 3.3 Hz (half the byte throughput into Ink).&lt;/li&gt;
&lt;li&gt;Static black 8-pixel border around the DOOM image — edge cells never change, so Ink's diff emits nothing for them. Free stable anchors at every line boundary.&lt;/li&gt;
&lt;li&gt;256-color palette instead of TrueColor (32-byte SGR → 16-byte SGR, half the volume). Doom's native palette is 256 colors anyway, so it's not a semantic loss.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;~270 KB/s into Ink instead of ~860 KB/s. Drift slowed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading Ink's source
&lt;/h3&gt;

&lt;p&gt;Ink uses &lt;a href="https://github.com/sindresorhus/log-update" rel="noopener noreferrer"&gt;&lt;code&gt;log-update&lt;/code&gt;&lt;/a&gt; internally. log-update's model is dead simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lastOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newOutput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lastOutput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`\x1b[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;A\x1b[0J`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// cursor up N, clear to end&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newOutput&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;lastOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newOutput&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;lastOutput&lt;/code&gt; is state. If log-update's model of "previous was 29 lines" gets out of sync with reality, subsequent updates clear the wrong region. This is the drift.&lt;/p&gt;

&lt;p&gt;There's no external API to invalidate log-update's cache. &lt;code&gt;rerender()&lt;/code&gt;, &lt;code&gt;clear()&lt;/code&gt;, &lt;code&gt;unmount()&lt;/code&gt; are methods on the Ink render instance &lt;em&gt;inside&lt;/em&gt; the app — you can't reach them from a shell script writing to the statusline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The only lever is &lt;code&gt;lastOutput.split('\n').length&lt;/code&gt;.&lt;/strong&gt; If you emit fewer lines, log-update moves up that smaller count, clears that smaller region, and your N−M previous lines are dropped from its cache.&lt;/p&gt;

&lt;h3&gt;
  
  
  The short-frame trick
&lt;/h3&gt;

&lt;p&gt;Every 20th statusline refresh, the script emits &lt;strong&gt;one line&lt;/strong&gt; (&lt;code&gt;[doom] — refreshing...&lt;/code&gt;) instead of the 29-line frame. Ink's log-update now thinks "previous output was 1 line." Next refresh, we emit 29 lines — log-update treats them as fresh content, re-anchoring its cache.&lt;/p&gt;

&lt;p&gt;It works. You see a brief text flash every 20 seconds, then DOOM is back with drift reset.&lt;/p&gt;

&lt;h3&gt;
  
  
  The multiplexer discovery
&lt;/h3&gt;

&lt;p&gt;Trying different multiplexers side-by-side:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Multiplexer&lt;/th&gt;
&lt;th&gt;Drift&lt;/th&gt;
&lt;th&gt;TrueColor&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;tmux&lt;/strong&gt; (properly configured)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;gone&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;re-parses ANSI into its own grid, re-emits everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zellij&lt;/td&gt;
&lt;td&gt;still drifts&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;modern, TrueColor by default, doesn't normalize aggressively enough&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screen&lt;/td&gt;
&lt;td&gt;drifts &lt;strong&gt;and&lt;/strong&gt; goes monochrome&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;worst of both&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;plain iTerm2 / Kitty / Ghostty / Alacritty&lt;/td&gt;
&lt;td&gt;drifts&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no protection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;tmux is architecturally special&lt;/strong&gt; for this use case. It parses all output into a canonical cell grid and re-synthesizes everything when emitting to the outer terminal. Ink's drifty cursor-up-and-clear instructions get absorbed; tmux emits its own clean state. Zellij and Screen nominally do the same but not aggressively enough.&lt;/p&gt;

&lt;p&gt;Running Claude Code inside tmux made drift disappear entirely, with zero code changes on our end.&lt;/p&gt;

&lt;h3&gt;
  
  
  The undocumented Claude Code env var
&lt;/h3&gt;

&lt;p&gt;tmux fixed drift but broke colors — DOOM came out in muted primary colors. Gradient tests showed tmux WAS passing TrueColor through correctly. The problem was Claude Code itself.&lt;/p&gt;

&lt;p&gt;Someone on GitHub had filed &lt;a href="https://github.com/anthropics/claude-code/issues/46146" rel="noopener noreferrer"&gt;issue #46146&lt;/a&gt;: Claude Code contains a defensive 256-color clamp whenever &lt;code&gt;$TMUX&lt;/code&gt; is set, specifically to guard against broken tmux configurations. The escape hatch is &lt;code&gt;CLAUDE_CODE_TMUX_TRUECOLOR=1&lt;/code&gt;. Must be exported in the shell; &lt;code&gt;settings.json&lt;/code&gt; is too late because the clamp runs at module load.&lt;/p&gt;

&lt;p&gt;With that env var, Claude Code's own UI emits TrueColor, our DOOM frame still uses 256-palette (for drift resistance), and we get the best of both: vivid Claude TUI, drift-resistant DOOM.&lt;/p&gt;

&lt;h3&gt;
  
  
  The gamma fix
&lt;/h3&gt;

&lt;p&gt;Final symptom: walls and corridors were too dark to tell what I was looking at. Not a drift or palette issue — a dynamic-range issue.&lt;/p&gt;

&lt;p&gt;DOOM's dark scenes use the entire RGB range 0–95 for shadow detail. Xterm's 256-color cube has only two levels per channel in that range (0 and 95). Everything darker than mid-gray collapses into palette index 0 (pitch black). Brick textures, corridor shading, enemy silhouettes in shadow — all collapsed into monochrome mud.&lt;/p&gt;

&lt;p&gt;Fix: a gamma curve applied to pixel values &lt;em&gt;before&lt;/em&gt; quantization, via a 256-entry LUT:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;GAMMA_LUT&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GAMMA_PRE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At &lt;code&gt;GAMMA_PRE=0.7&lt;/code&gt;, pixel value 64 maps to 95. Now shadow-range pixels land on cube entries 58 (muted olive), 95 (dark red), rather than all snapping to 0. Hue information returns to the render. Corridors are legible again.&lt;/p&gt;

&lt;p&gt;Zero extra bytes per frame (LUT is a table lookup, not per-frame work). No drift penalty.&lt;/p&gt;




&lt;h2&gt;
  
  
  The final ship config
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Plain iTerm2/Kitty/Ghostty/etc., no tmux:&lt;/span&gt;
/doom start &lt;span class="nt"&gt;--gamma&lt;/span&gt; 0.7

&lt;span class="c"&gt;# That's it. All the internal mitigations (byte-stable cells, static&lt;/span&gt;
&lt;span class="c"&gt;# black border, 256-palette default, fixed-width HUD, ordered dither,&lt;/span&gt;
&lt;span class="c"&gt;# regex-anchor fix, halved frame rate) are on by default. Occasional&lt;/span&gt;
&lt;span class="c"&gt;# drift under heavy damage-flash scenes is possible but rare.&lt;/span&gt;

&lt;span class="c"&gt;# If you want stability GUARANTEED under any scene change, run Claude&lt;/span&gt;
&lt;span class="c"&gt;# Code inside tmux:&lt;/span&gt;
&lt;span class="c"&gt;# ~/.tmux.conf&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; default-terminal &lt;span class="s2"&gt;"tmux-256color"&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-sa&lt;/span&gt; terminal-overrides &lt;span class="s2"&gt;",xterm*:Tc"&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-sa&lt;/span&gt; terminal-overrides &lt;span class="s2"&gt;",iterm*:Tc"&lt;/span&gt;
set-environment &lt;span class="nt"&gt;-g&lt;/span&gt; COLORTERM &lt;span class="s2"&gt;"truecolor"&lt;/span&gt;

&lt;span class="c"&gt;# ~/.zshrc (to restore TrueColor for Claude's own UI inside tmux)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CLAUDE_CODE_TMUX_TRUECOLOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plain setup is good enough for most play. tmux is the bulletproof version. Every other fix I listed above is still in the code as supporting infrastructure — without them, even tmux wouldn't be enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons transferable to anything Ink-hosted
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Byte stability is not render stability&lt;/strong&gt; when your bytes pass through someone else's stateful renderer. We got our output to 97% byte-identical. Drift still happened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the paths you think are hit, not the paths you think you wrote.&lt;/strong&gt; Half of round one was me byte-stabilizing the chafa code path while my own setup was running sextant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;string-width&lt;/code&gt; is a surprisingly load-bearing library&lt;/strong&gt; for any Ink-based TUI. Unicode 13 glyphs it doesn't recognize will silently break cell accounting. Stick to U+2580–U+259F for any terminal graphics work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared state files between multiple instances will bite you.&lt;/strong&gt; 15 Claude Code instances racing a single tick-counter file diluted our periodic repaint to nothing until we keyed it per-claude-pid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regex anchors on SGR parameters matter.&lt;/strong&gt; &lt;code&gt;(?&amp;lt;![0-9])&lt;/code&gt; saved us from &lt;code&gt;38;5;2&lt;/code&gt; being mis-parsed as a TrueColor RGB prefix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not all terminal multiplexers are created equal.&lt;/strong&gt; tmux is uniquely good at drift repair; Zellij and Screen aren't. If you write a terminal app and expect to run under Ink-heavy tools, tmux is the only multiplexer you can rely on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools you're running &lt;em&gt;on top of&lt;/em&gt; have their own defensive assumptions.&lt;/strong&gt; Claude Code's &lt;code&gt;CLAUDE_CODE_TMUX_TRUECOLOR=1&lt;/code&gt; is an env var the docs don't mention but whose presence is required if you want 24-bit color inside tmux. You find these by reading GitHub issues, not docs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Palette too coarse" is often "dynamic range compressed into one bucket."&lt;/strong&gt; When dark walls came out as monochrome mud, the answer wasn't a custom palette — it was applying a gamma curve so the pixel values occupied more of the cube's existing range. Stretch before you redefine.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Bonus — let Claude play it
&lt;/h2&gt;

&lt;p&gt;A prompt that reliably gets Claude through E1M1, using the MCP tool surface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are playing DOOM. Finish E1M1 by reaching the exit switch.

Tools: doom_state (json), doom_look (png), doom_move (keys), doom_burst (sequences),
doom_wait (ticks), doom_macro (named macros).

Play loop:
1. doom_state + doom_look → perceive the scene
2. One paragraph: what you see, HP/ammo, next objective
3. ONE short action (≤6 keys)
4. doom_wait 20 ticks
5. Repeat

Combat: HP&amp;lt;30 + enemy → retreat. Strafe-fire imps. Corner-peek zombiemen.
Stop when state.map changes to 2, HP=0, stuck 60s, or 50 cycles no progress.
Don't call /doom stop. One-line progress every 10 cycles.
Start now.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works. The same Claude Code whose defensive rendering clamp we spent 10 hours working around is the one that plays through the level.&lt;/p&gt;




&lt;h2&gt;
  
  
  Repo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/erkandogan/doom-in-claude-code" rel="noopener noreferrer"&gt;https://github.com/erkandogan/doom-in-claude-code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The installer sets up the statusline, hook, slash command, MCP server, and downloads Freedoom WADs. Tested on macOS; should work on Linux. &lt;code&gt;brew install chafa tmux&lt;/code&gt; is the only dependency story.&lt;/p&gt;

&lt;p&gt;Issues, PRs, questions welcome. Happy debugging.&lt;/p&gt;

</description>
      <category>terminal</category>
      <category>claude</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Claude Code's channel system is limited to one bot, one terminal — here's how I fixed it</title>
      <dc:creator>Erkan DOGAN</dc:creator>
      <pubDate>Tue, 14 Apr 2026 15:56:11 +0000</pubDate>
      <link>https://dev.to/erkandogan/claude-codes-channel-system-is-limited-to-one-bot-one-terminal-heres-how-i-fixed-it-1dj4</link>
      <guid>https://dev.to/erkandogan/claude-codes-channel-system-is-limited-to-one-bot-one-terminal-heres-how-i-fixed-it-1dj4</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbmx2h96lrsyyrh5yt5p3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbmx2h96lrsyyrh5yt5p3.gif" alt="Claude Code Agent Team working example with Oh My Team agents" width="800" height="505"&gt;&lt;/a&gt;&lt;br&gt;
Claude Code shipped an experimental channel system. The idea is great and we are familiar from other tools like OpenClaw — connect a messaging bot to your terminal so you can talk to Claude from your phone. But in practice, it's one bot connected to one terminal. Close the terminal, session dies. Want two projects? Two bots. There's no session management, no routing, no persistence.&lt;/p&gt;

&lt;p&gt;I've been using Claude Code as my primary dev tool for months. I wanted to manage multiple projects from my phone without setting up separate bots for each one. So I built a router on top of Claude Code's channel protocol.&lt;/p&gt;
&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Claude Code's channel feature gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One bot ↔ one terminal session&lt;/li&gt;
&lt;li&gt;Close the terminal, lose the session&lt;/li&gt;
&lt;li&gt;No way to run multiple projects through the same bot&lt;/li&gt;
&lt;li&gt;No session management (start, stop, list, switch)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're working on one thing at a time and sitting at your desk, it works fine. But I'm usually juggling 3-5 projects and I want to check on them from my couch, from my phone, from wherever.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Oh My Team&lt;/strong&gt; is a Claude Code plugin that adds three things:&lt;/p&gt;
&lt;h3&gt;
  
  
  1. A session router
&lt;/h3&gt;

&lt;p&gt;A lightweight router sits between your messaging platform and Claude Code sessions. One bot, multiple sessions, each isolated in its own thread/topic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Telegram Group: "Oh My Team Hub"
+-- General topic     ← Hub: start/stop/list sessions
+-- Topic: frontend   ← Bridge → Claude (frontend project)
+-- Topic: backend    ← Bridge → Claude (backend project)
+-- Topic: mobile     ← Bridge → Claude (mobile project)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnbs37szqsb4k6e9pe2hn.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnbs37szqsb4k6e9pe2hn.webp" alt="Oh My Team Telegram Group topic and subject showcase" width="800" height="604"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Or with Slack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Slack Channel: #omt
+-- Root messages      ← Hub: manage sessions
+-- Thread: "my-app"   ← Bridge → Claude (my-app)
+-- Thread: "backend"  ← Bridge → Claude (backend)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each project gets its own MCP bridge. Messages go directly from your chat thread to that Claude session — the hub never sees project messages, so there's zero extra token cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Persistent sessions via tmux
&lt;/h3&gt;

&lt;p&gt;Every session runs inside tmux. Close your laptop, sessions keep running. Come back, reattach. The hub manages the lifecycle — starting sessions, registering them with the router, tearing them down.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;omt hub add ~/projects/my-app   &lt;span class="c"&gt;# starts a session in tmux&lt;/span&gt;
omt hub attach my-app            &lt;span class="c"&gt;# jump in&lt;/span&gt;
&lt;span class="c"&gt;# Ctrl+B, D to detach&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or just type &lt;code&gt;start ~/projects/my-app&lt;/code&gt; in the Telegram General topic.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Multi-agent teams
&lt;/h3&gt;

&lt;p&gt;This part came from frustration with single-agent workflows. One agent doing research, planning, coding, and reviewing its own work is like one person doing all the roles on a team.&lt;/p&gt;

&lt;p&gt;Oh My Team has 12 specialized agent definitions. When you invoke &lt;code&gt;/oh-my-team:team&lt;/code&gt;, it spawns the right combination into tmux panes — you can watch each agent working in parallel.&lt;/p&gt;

&lt;p&gt;The one I actually use most: &lt;code&gt;/oh-my-team:review-work&lt;/code&gt;. It spawns 5 independent agents that review the code from different angles — goal verification, QA, code quality, security, and context mining. All 5 must pass. It catches things a single agent won't catch about its own output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/oh-my-team:team Build OAuth with RBAC

&lt;span class="c"&gt;# 5 tmux panes open:&lt;/span&gt;
&lt;span class="c"&gt;# explorer-1: researching existing auth patterns&lt;/span&gt;
&lt;span class="c"&gt;# librarian-1: checking OAuth best practices&lt;/span&gt;
&lt;span class="c"&gt;# hephaestus-1: implementing the feature&lt;/span&gt;
&lt;span class="c"&gt;# hephaestus-2: writing tests&lt;/span&gt;
&lt;span class="c"&gt;# reviewer: reviewing output&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How the router works
&lt;/h2&gt;

&lt;p&gt;The architecture is intentionally simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Router&lt;/strong&gt; (TypeScript, runs on Bun) — HTTP server on port 8800. Maintains a session registry. Receives messages from the platform adapter, looks up which session owns that thread, forwards to the right bridge.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Platform adapter&lt;/strong&gt; — implements a &lt;code&gt;ChannelAdapter&lt;/code&gt; interface (~150 lines). Currently Telegram (Forum Topics) and Slack (Socket Mode). Adding Discord would be one more adapter file.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bridge&lt;/strong&gt; (one per session) — MCP channel server that Claude Code connects to. Translates between the router's HTTP calls and Claude Code's channel protocol.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hub session&lt;/strong&gt; — a Claude Code session running the Hub agent. Listens on the General topic/channel root. It manages sessions via the &lt;code&gt;omt&lt;/code&gt; CLI — it's just Claude running bash commands.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Phone → Telegram/Slack → Adapter → Router → Bridge → Claude Code
                                                ↑
                                          MCP channel protocol
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: each bridge is an independent MCP server. The router just routes. Project sessions never share context. The hub session only handles management — "start X", "stop Y", "what's running".&lt;/p&gt;

&lt;h2&gt;
  
  
  Permission prompts
&lt;/h2&gt;

&lt;p&gt;This was the tricky part. Claude Code asks for permission before risky operations (file writes, bash commands). In a terminal, it's a prompt. Through channels, it needs to work asynchronously.&lt;/p&gt;

&lt;p&gt;When Claude asks for permission, the bridge forwards it to your chat thread with a code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude wants to: Run bash command "npm install express"
Reply: yes abc123 / no abc123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You reply from your phone, the bridge relays back, Claude continues. Works across all platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; oh-my-team          &lt;span class="c"&gt;# install&lt;/span&gt;
omt hub init                  &lt;span class="c"&gt;# interactive wizard: pick Telegram or Slack, paste tokens&lt;/span&gt;
omt hub start                 &lt;span class="c"&gt;# starts router + hub&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Slack, there's a one-click app manifest that pre-configures all the scopes and Socket Mode settings.&lt;/p&gt;

&lt;p&gt;For local use without any messaging platform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;omt &lt;span class="nt"&gt;-d&lt;/span&gt;                        &lt;span class="c"&gt;# start with auto-permissions&lt;/span&gt;
/oh-my-team:team &lt;span class="k"&gt;do &lt;/span&gt;the thing &lt;span class="c"&gt;# spawn agents&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I learned building this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Claude Code's channel protocol is actually well-designed.&lt;/strong&gt; The MCP-based approach made it straightforward to build bridges. The limitation is the 1:1 mapping, not the protocol itself.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Agents reviewing other agents' work produces genuinely better output.&lt;/strong&gt; One agent can't objectively evaluate its own code. Five independent reviewers catch real issues.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;tmux is underrated for AI agent management.&lt;/strong&gt; Split panes give you visibility into what each agent is doing. It's not a black box.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Token cost doesn't multiply with agents.&lt;/strong&gt; Each agent gets its own context. The hub only processes management commands. Project sessions are independent. You're not paying for a mega-context that includes everything.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/erkandogan/oh-my-team" rel="noopener noreferrer"&gt;github.com/erkandogan/oh-my-team&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Website: &lt;a href="https://ohmyteam.cc" rel="noopener noreferrer"&gt;ohmyteam.cc&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/oh-my-team" rel="noopener noreferrer"&gt;npmjs.com/package/oh-my-team&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's MIT licensed. The whole thing is Markdown files for agents/skills + a small TypeScript channel system. PRs welcome — especially if anyone wants to build the Discord adapter.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
