A lightweight, AI enabled, native C SSH client for Windows, focusing on performance, minimal dependencies, and native OS integration. Built entirely with the Win32 API — no external UI frameworks. Cross-compiled from Linux with MinGW-w64, the release binary is ~2.0 MB (UPX compressed).
A ready-to-run Windows executable is available at build/win/nutshell.exe — no compilation needed.
- Multi-tab SSH sessions with owner-drawn tab strip (status dots, log indicator, close button)
- VT100/ANSI terminal emulator — 256-colour, truecolor, alt screen, scroll regions, app cursor keys, OSC title
- Password and SSH key authentication with passphrase prompt and retry
- AES-256-GCM password encryption at rest (PBKDF2-SHA256 derived key, OpenSSL)
- TOFU host key verification (first-connect dialog, mismatch warning)
- Dynamic PTY resize on window resize and zoom
- Paste confirmation dialog with configurable inter-line delay
- Session file logging with ANSI stripping and configurable strftime filenames
- Onyx Synapse UI — 4 themed colour schemes with consistently themed tabs, dialogs, and buttons
- AI chat assistant — reads terminal context, executes commands, supports streaming and reasoning display
- DPI-aware layout across all windows and dialogs
- 865 unit tests, zero lint warnings
- Place
nutshell.exeanywhere on your Windows machine. Configuration is stored innutshell.configin the same directory. - Launch the application. The session manager opens automatically.
- Create a session profile by entering a hostname, port, username, and authentication credentials, then click Connect.
Open the Session Manager with Ctrl+T or by clicking the + area in the tab strip.
| Field | Description |
|---|---|
| Name | Optional friendly label (shown in the tab and tooltips) |
| Host | Hostname or IP address |
| Port | SSH port (default: 22) |
| Username | Login username |
| Auth Type | Password or SSH Key |
| Password / Key Path | Password for password auth, or path to private key file for key auth |
| Passphrase | Passphrase for encrypted SSH keys (shown when auth type is Key) |
| AI Notes | Per-session context notes sent to the AI assistant (optional) |
Saved profiles appear in the list on the left. Double-click a profile to connect immediately. Use Save to store changes, Delete to remove a profile.
On first connection to a new host, a host key verification dialog shows the server's fingerprint. Accept to save it; future connections verify against the stored key and warn if it changes.
Each SSH connection runs in its own tab. The tab strip at the top of the window shows all open sessions.
| Action | How |
|---|---|
| New tab | Ctrl+T opens the session manager |
| Close tab | Ctrl+W or click the x button on the tab |
| Switch tab | Click the tab |
| Tab tooltip | Hover over a tab to see session name, user@host, connection status with elapsed time, and logging status |
Each tab shows a status dot:
- Grey = idle/disconnected
- Yellow = connecting
- Green = connected
- Red = connection lost
A [L] badge appears when session logging is active for that tab.
The terminal emulates a VT100/ANSI-compatible display. It supports:
- 16, 256, and truecolor (24-bit RGB) rendering
- Bold, dim, underline, blink, reverse video text attributes
- Alternate screen buffer (used by vim, nano, less, htop, etc.)
- Scroll regions (DECSTBM — used by ncurses applications for smooth scrolling)
- Application cursor keys mode (programs like vim switch arrow key sequences)
- OSC title — programs can set the window/tab title via escape sequences
- 10,000-line scrollback by default (configurable 100 to 50,000)
- Mouse wheel scrolls through scrollback history
- Page Up / Page Down scroll one page at a time
- Vertical scrollbar on the right tracks the scrollback position (drag to seek)
- Click and drag on the terminal to select text
- Ctrl+V or Shift+Insert pastes from the clipboard
- Pasting more than 64 characters triggers a confirmation dialog showing a preview. This prevents accidental large pastes.
- Paste delay: an optional inter-line delay (0 to 5000 ms) can be set in Settings for servers that need time between lines
Zoom the terminal font in discrete steps: 6, 8, 10, 12, 14, 16, 18, 20 pt.
| Action | How |
|---|---|
| Zoom in | Ctrl+= or Ctrl+Mouse Wheel Up |
| Zoom out | Ctrl+- or Ctrl+Mouse Wheel Down |
Zoom changes trigger an automatic PTY resize so the remote shell adapts to the new column/row count.
Open from the menu or via the settings button. Changes take effect immediately — no restart needed.
- Font family — curated list of monospace fonts: Consolas (default), Cascadia Code, Cascadia Mono, Courier New, Inter, Lucida Console, Lucida Sans Typewriter, Fira Code, JetBrains Mono, Source Code Pro, Hack
- Font size — discrete sizes from 6 to 20 pt
- Colour scheme — choose from 4 built-in themes:
- Onyx Synapse (dark, default) — dark background with green accents
- Onyx Light — light background variant
- Sage & Sand — dark earthy tones
- Moss & Mist — light pastel colours
- Scrollback lines — 100 to 50,000 (default: 10,000)
- Paste delay — inter-line delay in milliseconds (0 to 5000)
- Enable/disable session file logging
- Log directory — where log files are saved (default: same directory as the executable)
- Log format — strftime format string for log filenames (e.g.
%Y-%m-%d_%H-%M-%S)
- Provider — Anthropic (default), OpenAI, Gemini, Moonshot, DeepSeek, or Custom
- API key — encrypted at rest with AES-256-GCM (same encryption as saved passwords)
- Custom URL / Model — for self-hosted or alternative API endpoints
- System notes — global instructions included in every AI conversation
Click the AI button in the tab strip to open the chat window. The button is green when an API key is configured, grey otherwise.
The AI assistant can see the last 50 lines of your terminal output and execute commands over SSH. Each tab maintains its own independent conversation history.
| Control | Function |
|---|---|
| New Chat | Clear the conversation and start fresh |
| Permit Write | Toggle read/write mode. Green = AI can execute any command. Red = AI restricted to read-only commands (ls, cat, pwd, etc.) |
| Show Thinking | Toggle display of AI reasoning/chain-of-thought (for models like DeepSeek Reasoner that provide it) |
| Save (disk icon) | Save the conversation as a plain text file |
| Context bar | Shows approximate token usage as a percentage of the model's context window |
- Type in the input box at the bottom
- Enter sends the message
- Shift+Enter inserts a newline (for multi-line messages)
- Click Send (or press Enter) to submit
When the AI suggests commands, they appear inline in the chat window with Allow/Deny buttons. You can:
- Allow — execute all queued commands in sequence
- Deny — reject the commands
After commands execute, the AI automatically reads the updated terminal output and continues the conversation, reporting results or running additional commands as needed.
Each saved session profile has an AI Notes field in the session manager. These notes are included in the AI's system prompt for that session, giving it context about the server (e.g. "This is the production database server, be cautious with write operations").
Global System Notes in Settings are included in every conversation across all sessions.
When enabled in Settings, each connected session writes a log file with ANSI escape codes stripped (plain text only).
- Log files are named using the configured strftime format (default:
%Y-%m-%d_%H-%M-%S_hostname.log) - The [L] badge on a tab indicates active logging
- Logging status is also shown in tab tooltips
- Passwords and API keys are encrypted at rest in
nutshell.configusing AES-256-GCM with a PBKDF2-SHA256 derived key - Host key verification follows a Trust-On-First-Use (TOFU) model. Known hosts are stored at
%APPDATA%\sshclient\known_hosts. A mismatch triggers a warning dialog (possible man-in-the-middle) - SSH key passphrases are cached in memory only for the duration of the session and securely zeroed on close
| Shortcut | Action |
|---|---|
| Ctrl+T | New session (open session manager) |
| Ctrl+W | Close active tab |
| Ctrl+V | Paste from clipboard |
| Shift+Insert | Paste from clipboard (alternative) |
| Ctrl+= | Zoom in |
| Ctrl+- | Zoom out |
| Ctrl+Scroll | Zoom in/out with mouse wheel |
| Page Up | Scroll up through scrollback |
| Page Down | Scroll down through scrollback |
.
├── src/
│ ├── core/ # Portable logic — xmalloc, vector, string_utils, logger, tab_manager,
│ │ # theme, ui_theme, tooltip, snap, zoom, connect_anim, log_format,
│ │ # ai_prompt, ai_http, term_extract, display_buffer, app_font,
│ │ # selection, paste_preview, edit_scroll
│ ├── config/ # JSON tokenizer, JSON parser, config loader, ssh_io
│ ├── crypto/ # AES-256-GCM password encryption (OpenSSL)
│ ├── term/ # Terminal emulator (buffer, parser) + SSH (session, channel, PTY, knownhosts)
│ ├── ui/ # Win32 UI — renderer, tabs, window, session manager, settings dialog,
│ │ # ai_chat, ai_dock, ai_http_win, help_guide, paste_dlg, markdown,
│ │ # menubar_line, themed_button, custom_scrollbar
│ └── main.c
├── tests/ # Unit tests (TDD — tests written before implementation)
├── build/ # Build artefacts (gitignored)
├── PRD.md # Product Requirements
├── agents.md # Lessons learned and tips for contributors
└── Makefile
- GCC (MinGW-w64) —
x86_64-w64-mingw32-gccfor Windows cross-compile, nativegccfor tests g++-mingw-w64-x86-64— required by vcpkg even for C-only builds- Make
- cppcheck (static analysis)
- vcpkg with the custom
x64-mingw-gcc-statictriplet for MinGW-targeted OpenSSL and libssh2 (see~/vcpkg/custom-triplets/)
Always use make release for the distributable build — it compiles with size optimisations and compresses with UPX (~2.0 MB):
make releaseRequires upx (sudo apt install upx). Use plain make only if you need an uncompressed binary for debugging.
| Command | Purpose |
|---|---|
make release |
Recommended — optimised + UPX compressed (~2.0 MB) |
make |
Uncompressed build (~6.7 MB), useful for debugging |
make test |
Run unit tests (native Linux) |
make lint |
Static analysis with cppcheck |
make debug |
Build with AddressSanitizer + UndefinedBehaviorSanitizer |
make clean |
Remove all build artefacts |
To run Dr. Memory on the Windows release build:
- Build the executable:
make - Transfer
build/win/nutshell.exeto a Windows machine. - Run with Dr. Memory:
drmemory.exe -- nutshell.exe
- No External UI Libs: We use raw Win32 API.
- Memory Management: strict
malloc/freediscipline. Use the customxmallocwrapper insrc/core.xmallocaborts on OOM — callers treat the return value as unconditionally valid. - TDD: Create a test file in
tests/before writing code insrc/. - Static Analysis: Run
make lintbefore committing. All code must pass with zero warnings. cppcheck enforces const-correctness viaconstVariablePointer— declare pointers asconst T *whenever the pointee is not mutated. - No format-string vulnerabilities:
log_write()and any new logging functions take a pre-formattedconst char *. Usesnprintfat the call site, not variadic format arguments inside library functions. - String copying: Always use
snprintf(dst, sizeof(dst), "%s", src)for fixed-size field copies. Never use barestrcpyorstrncpy.
If you are an AI assistant helping with this codebase, please observe the following:
- Context is King: Always read the header files (
.h) first to understand struct definitions before modifying implementation (.c) files. - Memory Safety: C does not have garbage collection. Every
mallocmust have a correspondingfree. When suggesting code, always double-check error paths (e.g., if a socket fails, is the buffer freed?). Usemake debugto compile with AddressSanitizer and catch issues at runtime. - Win32 API Verbosity: The Win32 API is verbose. When writing UI code, prefer creating helper functions for repetitive tasks like
CreateWindowExorSendMessage. - Test Harness: We use a custom minimal test runner. It relies on macros like
ASSERT_EQandASSERT_TRUE. Do not importCUnitorGoogleTest; use the providedtests/test_framework.h. Each test function usesTEST_BEGIN()/TEST_END()— failures are reported but do not abort the function, so all assertions in a test always run. - String Handling: Do not use
strcatorstrcpy. Usesnprintf(dst, sizeof(dst), "%s", src)for field copies, or the helpers insrc/core/string_utils.h(str_dup,str_cat,str_trim). - Cross-Reference: If you are unsure about logic, check
../golang/for the reference Go implementation, but translate the intent, not the syntax. Go channels usually map to Windows Events or callback queues in C. - Struct Packing: Be mindful of struct padding and alignment when dealing with network protocols (SSH packets). Use
#pragma packif necessary. - JSON / Config API: Use
json_parse()->json_obj_get()/json_obj_str()/json_obj_num()/json_obj_bool()to extract values. Always calljson_free()on the root when done. Config fields are fixed-sizechar[256]arrays — no heap allocation per field. Load withconfig_load(), save withconfig_save(), and always callconfig_free()on the returned pointer. - Const correctness: cppcheck enforces
constVariablePointer. Declare local pointers asconst T *if the pointee is not mutated.json_obj_get(),json_obj_str(),json_obj_num(), andjson_obj_bool()all acceptconst JsonNode *, so callers can pass const pointers directly. - Secrets hygiene:
config_profile_free()zeroesProfile.passwordwithmemsetbefore callingfree. Follow the same pattern for any other field that may hold credentials. - Module layout: SSH code lives in
src/term/(ssh_session.c, ssh_channel.c, ssh_pty.c, knownhosts.c), not a separatesrc/ssh/directory. Crypto is insrc/crypto/. Tab management logic is insrc/core/tab_manager.c; the owner-drawn tab strip UI is insrc/ui/tabs.c. - libssh2 macros:
libssh2_channel_open_session,libssh2_session_init, andlibssh2_channel_writeare macros in the real libssh2 header.src/term/libssh2.his a stub used only for test builds — the real header comes from vcpkg at~/vcpkg/installed/x64-mingw-gcc-static/. - Resize reflow:
TermRow.lentracks actual written content width. When reflowing on resize, always loop torow->len, notterm->cols, to avoid copying trailing empty cells that cause spurious wraps. - Discrete font sizes: Allowed sizes are
{6, 8, 10, 12, 14, 16, 18, 20}— centralised insrc/core/app_font.h(APP_FONT_SIZESarray,APP_FONT_NUM_SIZES). Useapp_font_snap_size()to snap any out-of-set value to the nearest allowed size. Default is 10 pt. - Colour defaults: Default terminal colours are
fg=#E0E0E0, bg=#121212matching the "Onyx Synapse" colour scheme.COLOR_DEFAULTfg/bg mode means the renderer substitutes the configured scheme colours; hardcoded colour values inbuffer.c/parser.care only set forCOLOR_ANSI16/COLOR_256/COLOR_RGBcells. - UI theming: All UI chrome (tabs, settings, session manager, AI chat) is themed via
ThemeColorsfromsrc/core/ui_theme.{c,h}. UseWM_CTLCOLORDLG,WM_CTLCOLORSTATIC,WM_CTLCOLOREDIT,WM_CTLCOLORLISTBOXto paint dialog backgrounds and control colours. Buttons useBS_OWNERDRAWwithdraw_themed_button()fromthemed_button.h. The theme is looked up by name from config (colour_schemefield) viaui_theme_find()+ui_theme_get(). - Scrollbar:
update_scrollbar()inwindow.csyncs a Win32 vertical scrollbar to the active terminal. UseGetScrollInfo(SIF_TRACKPOS)forWM_VSCROLL— neverHIWORD(wParam), which silently truncates to 16 bits and breaks scrollback > 65535 lines. - AI integration architecture: AI code is split into portable core (
src/core/ai_prompt.c,src/core/ai_http.c,src/core/term_extract.c) and Win32 UI (src/ui/ai_chat.c,src/ui/ai_http_win.c). Core files are testable on Linux; UI files are excluded from test builds via theNON_TEST_SRCSpattern in the Makefile. The AI uses an OpenAI-compatible chat completion API (Anthropic default). Conversations useAiConversationstruct with role-tagged messages. Commands are extracted via[EXEC]cmd[/EXEC]markers. The HTTP client uses WinHTTP on Windows. - Two config.h files:
include/config.his used by the Windows cross-compile;src/config/config.his used by native test builds. Both must be kept in sync when modifying theSettingsstruct. - Config path caveat:
nutshell.configis loaded relative to CWD, which can change afterGetOpenFileNameAfile dialogs. The long-term fix is to resolve to an absolute path at startup usingget_exe_dir().