Nintendo Switch homebrew client for Claude — a chat app that talks to the
Anthropic Messages API from a
hacked Switch, built with devkitPro/libnx. Produces a .nro that runs under
Atmosphère custom firmware.
v0.2.1 replaces the v0.1.x plain-terminal UI with a modern, touch-first
graphical interface in the style of the Homebrew Menu: chat bubbles, card
layout, big touch targets, inertial scrolling, and a header/status bar —
with full d-pad/stick/button navigation alongside touch. Rendering is a
custom software compositor (surfaces + SDL_ttf text) presented through the
libnx NWindow/Framebuffer path — no GPU/EGL is used on-device. The system
swkbd applet is used for text entry (native touch keyboard), and USB HID
keyboards still work.
v0.2.0 hung on the emulator "Launching..." screen: the devkitPro SDL2 port forces
SDL_WINDOW_OPENGLand creates an EGL/nouveau surface for every window — GPU init can block forever under emulators. v0.2.1 presents via the native framebuffer instead (the same mechanismconsoleInitused in v0.1.0), mounts RomFS before font loading, and writes a boot log tosdmc:/config/claude-code-switch/debug.log.
This is a chat client: a working single-shot-per-turn client with the plumbing in place (TLS, conversation state, scrollback, keyboard input, SD-card settings). It is not the full Claude Code agentic tool loop — see Implemented vs. stubbed.
These were rendered by the app's own UI code (source/gfx.c + source/ui.c)
built against desktop SDL2 via tools/hostshot — they are pixel-accurate to
the drawing code, but not photographs of real hardware.
| Chat | Thinking | Settings | Empty state |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
- Graphical chat UI (software compositor + SDL_ttf, Inter font, 1280×720+): rounded-corner chat bubbles (user right/accent, assistant left/card, system + error chips centered), per-sender labels, typing indicator, header bar with title + model chip + live status pill, icon buttons, compose bar with draft field + Send button, hint strip, toast notices, and an empty-state onboarding card.
- Touch input: tap to activate (fields, buttons, settings rows), drag to scroll the transcript with inertial flick, "jump to latest" chip when scrolled up. All targets are ≥ 52 px.
- D-pad / stick / button navigation: d-pad left/right moves a focus ring across interactive elements (input field, Send, clear, settings, quit); A activates; d-pad up/down or either stick scrolls; L/R (or ZL/ZR) page up/down. Same model on the settings screen (rows + buttons).
- On-screen keyboard:
swkbd(the HOS system keyboard) is used for the compose field and every settings field — the native, touch-capable keyboard the OS provides. - USB keyboard input:
hidGetKeyboardStatespolling (HOS 9.0.0+ USB HID keyboard support) — type directly into the compose field, Enter sends, Backspace edits, Esc cancels an in-flight request. US layout with Shift. - HTTPS networking: BSD sockets + libcurl + mbedTLS via devkitPro
portlibs. TLS certificates are verified against a Mozilla CA bundle baked
into the NRO's RomFS (
romfs/cacert.pem) — no verification bypass. - Anthropic API integration:
POST {base_url}/v1/messageswithx-api-key+anthropic-version: 2023-06-01, JSON built/parsed with cJSON. Multi-turn context: the whole conversation is resent each request (as the API requires). - Non-blocking requests: the curl call runs on a worker thread; the UI
stays responsive with a typing indicator, and
B/Esc cancels via the curl progress callback. - Settings screen: card list for API key (masked display), model name,
and base URL, persisted as JSON to
sdmc:/config/claude-code-switch/settings.json. - Build: standard devkitPro Makefile →
claude-code-switch.nro, plus a GitHub Actions job that builds it inside thedevkitpro/devkita64container, uploads the.nroas an artifact, and creates a GitHub Release with the.nroattached onv*tags.
- Claude Code agent loop / tool use: the real Claude Code runs a read-eval tool-use loop (file ops, shell, etc.). This client exposes no tools — it sends a plain conversation, so the model can only answer with text. Tool calling is the obvious next milestone.
- Streaming (SSE): requests use
"stream": false; the reply appears only when complete. SSE parsing via the curl write callback is straightforward to add. - Bluetooth keyboards: HOS does not expose generic Bluetooth HID
keyboards to homebrew; only USB HID keyboards (via
hid) are read. - Input method niceties: no mid-line cursor movement on USB keyboard (append/backspace only), no key repeat, no non-US layouts.
- Conversation persistence: chat history lives in RAM only; clearing (X/trash) or quitting loses it. The settings file is the only persisted state.
- Proxy/custom auth:
base_urlis configurable, which covers simple API-compatible gateways, but there is no OAuth, no custom-header support, noanthropic-betaflags. - Markdown/code rendering: replies render as plain wrapped text; code blocks are not syntax-highlighted.
- The key is stored in plaintext at
sdmc:/config/claude-code-switch/settings.json. Anything that can read the SD card (any homebrew, a PC, another console) can read the key. - The key travels to
base_urlonly — it is never sent anywhere else, and the code logs nothing. TLS is verified, so it is not sniffable on the wire to api.anthropic.com. - Recommendation: create a dedicated Anthropic API key for this app,
set a hard spend limit on it, and revoke it when done. Do not point
this at untrusted
base_urls — that would hand your key to a third party. - If the SD card is shared/lost, rotate the key.
| Input | Action |
|---|---|
| Touch: tap | Activate buttons/fields; tap compose field to open the keyboard |
| Touch: drag | Scroll the transcript (with inertia) |
A |
Activate focused element (keyboard on the compose field) |
B / Esc |
Cancel in-flight request / back |
X / trash icon |
Clear conversation |
- / sliders icon |
Settings screen |
+ / power icon |
Quit |
| D-pad ↑/↓ | Scroll transcript one step |
| D-pad ←/→ | Move focus between interactive elements |
| Either stick | Smooth-scroll transcript |
L/R, ZL/ZR |
Page up / page down |
| USB keyboard | Type into compose field; Enter sends |
Requires a Switch running Atmosphère (any recent version; tested target is HOS 9.0.0+ for USB keyboard support — the app itself runs on older firmware but USB keyboard input won't appear).
- Grab
claude-code-switch.nrofrom Releases and copy it tosdmc:/switch/claude-code-switch/. - Launch via the Homebrew Menu (hbmenu) — full RAM mode recommended
(hold
Ron a game, not applet/album mode, so curl + history have heap). - Open Settings (
-or the sliders icon), enter your Anthropic API key, Save & apply. - Tap the compose field (or press
A), type a message, send.
Networking requires the console to be online; the app performs real DNS +
TLS to api.anthropic.com (or your configured base_url).
Toolchain: devkitA64 + libnx (devkitPro). All non-libnx libs ship in
the devkitpro/devkita64 image: curl, mbedtls, zlib, SDL2,
SDL2_ttf (+ freetype, harfbuzz, libpng, bzip2, EGL, GLESv2,
drm_nouveau).
docker run --rm -v "$PWD:/work" -w /work devkitpro/devkita64 make -j"$(nproc)"# https://devkitpro.org/wiki/Getting_Started — install devkitA64 + switch-dev
sudo dkp-pacman -S switch-dev switch-curl switch-sdl2 switch-sdl2_ttf
export DEVKITPRO=/opt/devkitpro
make -j"$(nproc)"Output: claude-code-switch.nro in the repo root. make clean resets.
CI: .github/workflows/build.yml builds the same Docker command on every
push, publishes the .nro as a build artifact, and creates a GitHub
Release with the .nro attached on v* tags.
tools/hostshot/ builds gfx.c + ui.c against desktop SDL2 and saves
the frames under docs/ — used for the screenshots above. Requires
libsdl2-dev libsdl2-ttf-dev libsdl2-image-dev:
./tools/hostshot/build.sh docsNot verified on real hardware. The .nro compiles and links cleanly
with warnings addressed, and the screenshots above prove the actual
drawing/layout code, but the app has not been launched on a physical
Switch under Atmosphère, and the Anthropic request path has not been
exercised from a console. Known-risk areas to check on first hardware run:
- Framebuffer presentation (
nwindowGetDefault+framebufferCreate+framebufferMakeLinear) — same mechanism as libnxconsoleInit. swkbdoverlay compositing over the app's framebuffer.- libnx touch coordinate space vs. the framebuffer size (clamped; should be 1:1 at 1280×720 handheld and 1920×1080 docked).
- USB keyboard detection timing (
hidGetKeyboardStatesfirst-frame deltas). - TLS handshake latency / CN+SAN handling via the Switch mbedTLS port.
- Software composition + linear framebuffer copy cost per frame in applet mode — use full-RAM (title takeover) launch as noted above.
Emulator status: v0.2.0 was reported to hang on Eden's "Launching..."
screen (SDL2's mandatory EGL/nouveau window init is the implicated path);
v0.2.1 avoids GPU init entirely, and if startup still stalls the stage is
recorded in sdmc:/config/claude-code-switch/debug.log (also streamed over
nxlink). Ryujinx/yuzu do not emulate hid USB keyboards, real touch
events, or real network DNS faithfully for homebrew; they were not used.
source/main.c app loop, input plumbing (pad/touch/USB kbd), dispatch
source/gfx.c drawing layer: software compose (AA rounded rects,
gradients, SDL_ttf text, clipping) presented via libnx
NWindow/Framebuffer on Switch, SDL2 renderer on desktop
source/diag.c boot log: stderr/nxlink + sdmc debug.log
source/ui.c chat + settings screens: bubbles, focus nav, touch,
inertial scroll, toasts, typing indicator
source/net.c lazy socket init + curl worker thread, request/response,
conversation store
source/kbd.c swkbd wrapper + USB HID keyboard polling
source/settings.c SD-card settings JSON
source/cJSON.c vendored cJSON v1.7.18 (MIT)
romfs/ Inter fonts (OFL) + Mozilla CA bundle (TLS verify)
tools/hostshot/ desktop SDL2 screenshot harness (not part of the .nro)
Makefile devkitPro/libnx .nro build
- cJSON © Dave Gamble, MIT (vendored in
source/cJSON.c,include/cJSON.h). - Mozilla CA bundle © Mozilla, MPL-2.0 (
romfs/cacert.pem). - Inter font © The Inter Project Authors, SIL OFL 1.1
(
romfs/Inter-*.ttf, license inlicenses/Inter-LICENSE.txt). - Everything else: MIT. Homebrew for interoperability; not affiliated with Anthropic or Nintendo.



