winx-code-agent

gabrielmaialva33/winx-code-agent
30 starsMITCommunity

Install to Claude Code

This server doesn't publish a one-line install command. Follow the setup in the source repository.

Summary

A high-performance Rust reimplementation of WCGW for code agents, providing shell execution and advanced file management capabilities for LLMs via MCP.

README.md

<p align="center"> <img src=".github/assets/fairy.png" alt="Winx fairy mascot" width="160" /> </p>

✨ Winx - MCP Server for Shell & Coding Agents ✨

<p align="center"> <strong>🦀 Native Rust implementation inspired by WCGW, built for local code-agent workflows</strong> </p>

<p align="center"> <img src="https://img.shields.io/badge/language-Rust-orange?style=flat&logo=rust" alt="Language" /> <img src="https://img.shields.io/badge/license-MIT-blue?style=flat" alt="License" /> <img src="https://img.shields.io/badge/MCP-compatible-purple?style=flat" alt="MCP" /> <img src="https://img.shields.io/badge/transport-stdio-2f855a?style=flat" alt="stdio transport" /> </p>

<p align="center"> <em>A local MCP server you can hand to a coding agent and stop worrying about the shell.</em> </p>

Winx is the MCP server I wanted while running Claude, Codex, and friends against real repos: one process that handles the shell, file IO, and PTY-backed interactive sessions, written in Rust so it doesn't fight you on stdio.

It started as a Rust port of WCGW but isn't a Python wrapper. Everything runs on a real PTY (via portable-pty), cd actually sticks, Ctrl+C actually interrupts, and background shells survive long-running TUIs without leaking output buffers into your token budget.

What you get

  • A stateful bash session per thread with proper PTY semantics - foreground, background, status checks, text input,

Enter/Ctrl-C/Ctrl-D, raw ASCII. Multiline scripts and top-level command shorthand both work; NUL bytes are rejected before they reach the shell.

  • Workspaces with three modes: wcgw (full access), architect (read-only), code_writer (allowlist of commands and

write globs). The command allowlist is parsed with tree-sitter, so it checks every command on the line - pipelines, &&/||/;, command substitution, subshells - not just the first word, and can't be bypassed with ls && curl … | sh or ls $(rm …).

  • A resilient PTY: a shell that won't return to a prompt (even after Ctrl-C) is auto-reset at the same cwd/mode, child

processes are reaped on drop, and prompt detection is robust to a custom PS1. Opt into zsh with WINX_SHELL=zsh.

  • File reads with WCGW-style line ranges (file.rs:10-40, file.rs:10-, file.rs:-40). Active files are tracked

and prioritized in the repository context across calls.

  • File writes and SEARCH/REPLACE edits that survive ambiguous matches, indentation drift, and the usual unicode

quote-mismatches from LLMs. Writes are blocked when the file hasn't been read or the cached content is stale, the success message shows a compact diff of what changed, and recent edits are reversible with UndoEdit. MultiFileEdit applies a change across several files all-or-nothing (validated in memory first, so a failure on the last file leaves the earlier ones untouched).

  • Tree-sitter code navigation via CodeMap: a token-budgeted symbol map of a file or the whole repo, or a

definition/reference lookup for a symbol name - the semantic view that plain grep can't give you, across 11 languages.

  • ContextSave for handing a task summary plus its files to the next session - including workspace context, active

files, git status/diff, and terminal sharing for proper resumption. Resuming reopens the saved project root and token-caps the restored memory so it never overflows the context window.

  • ReadImage so multimodal clients can pull screenshots, mockups, error PNGs, etc.
  • Clean, token-aware shell output: cursor/ANSI noise from interactive programs (REPLs, progress

bars) is rendered away through a terminal emulator, and mechanical repetition is collapsed losslessly (line [winx: ×N]) so build/install logs don't blow your context budget. Toggle the collapsing with WINX_NO_COMPRESS. When output still overflows the cap, the dropped head is streamed to a scratch file under .winx/scratch/ the agent can re-read, instead of being lost.

  • Secret redaction on by default: provider API keys, JWTs, PEM private-key blocks and user:pass@ URLs

are scrubbed from all tool output and saved memory before they reach the model (disable with WINX_NO_REDACT=1). An opt-in Landlock sandbox (WINX_SANDBOX=1, Linux) adds a kernel-enforced second layer that confines writes to the workspace and hides the home directory.

  • Two transports: stdio for local clients, plus an optional token-gated Streamable HTTP server

(winx serve --http) for remote MCP clients like ChatGPT - see Remote access.

MCP Tools

| Tool | What it does | |-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Initialize | Boots the workspace, picks the mode, hands you a thread_id. Call this first or everything else errors out. With no workspace path it spins up a scratch playground; resuming a task (task_id_to_resume) reopens its saved project root. | | BashCommand | Runs commands, polls long-running ones, sends Enter/Ctrl-C, drives TUIs. Supports is_background, status_check, send_text, send_specials, send_ascii, allow_multi, plus screen (a stable point-in-time frame of an interactive TUI with the cursor position; pass diff:true for only the lines that changed since your last look) and wait_for_turn (block until the TUI is ready for input, via per-app or configurable recognizers). When a foreground command finishes, the status line reports its real exit code (parsed from the prompt marker), so failures surface without grepping stderr. | | ReadFiles | One or many files, with line numbers. Append :10-40 to a path for a range. When the token budget is hit it tells you the exact line + file:N-M syntax to resume from instead of silently dropping the tail. | | FileWriteOrEdit | Full overwrites or SEARCH/REPLACE blocks (with optional @start-end line anchors to pin a repeated block). Validates file read coverage and freshness before writing, reports any fuzzy tolerances it had to apply, then runs a tree-sitter syntax check (18+ languages) and points at the offending line with a snippet. The success message includes a compact diff of what changed. | | MultiFileEdit | Edits several files all-or-nothing: every file's edit is validated and computed in memory first, and only if all succeed is anything written - so a SEARCH that fails to match in the last file leaves the earlier ones untouched. For a single file use FileWriteOrEdit. | | UndoEdit | Reverts a file to its content before the last FileWriteOrEdit/MultiFileEdit this session (per-file, last ~10 edits kept in memory). Refused if the file changed on disk since your edit; a brand-new file's creation isn't undoable. | | ContextSave | Dumps task description + file globs into a single text file with workspace context, active files, and git status/diff for clean handoff and task resumption. | | ReadImage | Returns a native MCP image content block (not base64 as text), so multimodal models actually see the image. Confined to the workspace (like ReadFiles) and size-capped. | | CodeMap | Tree-sitter code navigation, in one tool with two operations. outline: a symbol map (functions, types, methods, ...) - a file returns its definitions, a directory (or empty) a relevance-ranked, token-budgeted repo symbol map, in 11 languages. references: where a name is defined and used (called) across the repo, counting only real identifier occurrences (never inside strings/comments, unlike grep), definitions first. For plain-text/regex search and file discovery, just use rg/fd/grep via BashCommand. |

Search/Replace editing

Standard block syntax:

<<<<<<< SEARCH
old content
=======
new content
>>>>>>> REPLACE

Things the matcher forgives so you don't have to babysit the model:

  • atomic: ambiguous or missing matches abort without touching the file
  • adjusts replacement indentation when the LLM gets the leading whitespace wrong
  • strips ReadFiles line numbers if they leak into a SEARCH block
  • normalizes the usual "smart quote" / em-dash / ellipsis substitutions
  • uses neighboring blocks to disambiguate when the same snippet appears twice
  • single-line substring edits work - you don't need the whole line in SEARCH
  • retries once with \" unescaped when the model over-escapes quotes in SEARCH
  • refuses edits that only matched after too much fuzzy fixup, and rejects blocks

that match in too many places - so you re-read instead of corrupting the file

  • anchor a block to a line number to pin one of several identical snippets -

<<<<<<< SEARCH @42 (or a range @42-50); a stale anchor falls back to the normal search, so it never fails an otherwise-valid edit

  • tells you on success which tolerances it had to apply (so you learn your

SEARCH drifted), and on a miss how close the nearest match was, with ~ marking the lines that diverged

Install

cargo install winx-code-agent

Binary lands in ~/.cargo/bin - every config snippet below assumes that's on $PATH. If your MCP client launches with a sterile env, swap winx-code-agent for the absolute path (which winx-code-agent).

Needs Rust 1.75+, Linux/macOS/WSL2, and a real terminal (any modern one - Winx spawns its own PTY).

<details> <summary><b>Claude Code (CLI)</b></summary>

One-liner via the CLI (stdio is the default transport):

claude mcp add winx -- winx-code-agent

Or drop a .mcp.json in your project root:

{
  "mcpServers": {
    "winx": {
      "command": "winx-code-agent",
      "env": { "RUST_LOG": "winx_code_agent=info" }
    }
  }
}

</details>

<details> <summary><b>Claude Desktop</b></summary>

Add to your config file (~/Library/Application Support/Claude/claude_desktop_config.json on macOS, %APPDATA%\Claude\claude_desktop_config.json on Windows):

{
  "mcpServers": {
    "winx": {
      "command": "winx-code-agent",
      "env": { "RUST_LOG": "winx_code_agent=info" }
    }
  }
}

Restart Claude Desktop after saving. </details>

<details> <summary><b>Codex (OpenAI CLI)</b></summary>

One-liner:

codex mcp add winx -- winx-code-agent

Or edit ~/.codex/config.toml:

[mcp_servers.winx]
command = "winx-code-agent"
env = { RUST_LOG = "winx_code_agent=info" }

</details>

<details> <summary><b>Cursor</b></summary>

Add to ~/.cursor/mcp.json (or .cursor/mcp.json for project-local):

{
  "mcpServers": {
    "winx": {
      "command": "winx-code-agent",
      "env": { "RUST_LOG": "winx_code_agent=info" }
    }
  }
}

</details>

<details> <summary><b>VS Code (Copilot Chat / MCP)</b></summary>

Add to .vscode/mcp.json:

{
  "servers": {
    "winx": {
      "type": "stdio",
      "command": "winx-code-agent"
    }
  }
}

</details>

<details> <summary><b>Zed</b></summary>

Add to your Zed settings (~/.config/zed/settings.json):

{
  "context_servers": {
    "winx": {
      "source": "custom",
      "command": "winx-code-agent",
      "args": [],
      "env": { "RUST_LOG": "winx_code_agent=info" }
    }
  }
}

</details>

<details> <summary><b>Windsurf</b></summary>

Add to ~/.codeium/windsurf/mcp_config.json:

{
  "mcpServers": {
    "winx": {
      "command": "winx-code-agent",
      "env": { "RUST_LOG": "winx_code_agent=info" }
    }
  }
}

</details>

<details> <summary><b>OpenCode</b></summary>

Add to opencode.json:

{
  "mcp": {
    "winx": {
      "type": "local",
      "command": ["winx-code-agent"],
      "enabled": true,
      "environment": { "RUST_LOG": "winx_code_agent=info" }
    }
  }
}

</details>

<details> <summary><b>Gemini CLI</b></summary>

Add to ~/.gemini/settings.json:

{
  "mcpServers": {
    "winx": {
      "command": "winx-code-agent",
      "args": [],
      "env": { "RUST_LOG": "winx_code_agent=info" }
    }
  }
}

</details>

<details> <summary><b>agy (Google Antigravity CLI)</b></summary>

agy is Google's new Gemini-powered CLI (Go binary, usually at ~/.local/bin/agy). No mcp add subcommand yet - it reads MCP servers from JSON.

Edit ~/.gemini/config/mcp_config.json (also ~/.gemini/antigravity/mcp_config.json if you run the Antigravity IDE alongside):

{
  "mcpServers": {
    "winx": {
      "command": "winx-code-agent",
      "env": { "RUST_LOG": "winx_code_agent=info" }
    }
  }
}

If winx-code-agent is not on the agy process $PATH, swap command for the absolute path (~/.cargo/bin/winx-code-agent after cargo install winx-code-agent). </details>

<details> <summary><b>Continue.dev</b></summary>

Add to your ~/.continue/config.yaml:

mcpServers:
  - name: winx
    command: winx-code-agent
    env:
      RUST_LOG: winx_code_agent=info

</details>

<details> <summary><b>Kiro</b></summary>

Add to ~/.kiro/settings/mcp.json:

{
  "mcpServers": {
    "winx": {
      "command": "winx-code-agent",
      "env": { "RUST_LOG": "winx_code_agent=info" }
    }
  }
}

</details>

<details> <summary><b>Warp</b></summary>

Settings → MCP Servers → Add MCP Server:

{
  "winx": {
    "command": "winx-code-agent",
    "env": { "RUST_LOG": "winx_code_agent=info" }
  }
}

</details>

<details> <summary><b>Roo Code</b></summary>

Add to your Roo Code MCP config:

{
  "mcpServers": {
    "winx": {
      "type": "stdio",
      "command": "winx-code-agent"
    }
  }
}

</details>

<details> <summary><b>Other clients (generic stdio)</b></summary>

Any client that speaks stdio MCP works with this shape:

{
  "mcpServers": {
    "winx": {
      "command": "winx-code-agent",
      "args": [],
      "env": { "RUST_LOG": "winx_code_agent=info" }
    }
  }
}

If your client launches Winx with an empty $PATH, swap command for the absolute path ( ~/.cargo/bin/winx-code-agent). </details>

<details> <summary><b>Build from source</b></summary>

For unreleased changes or a custom build:

git clone https://github.com/gabrielmaialva33/winx-code-agent.git
cd winx-code-agent
cargo install --path .

Or run it without installing:

cargo run --release

</details>

Check it's wired up

List MCP tools in your client. You should see nine entries: Initialize, BashCommand, ReadFiles, FileWriteOrEdit, MultiFileEdit, UndoEdit, ContextSave, ReadImage, CodeMap. The first call always has to be Initialize; Winx tracks workspace + mode per thread.

Remote access (ChatGPT & other remote MCP clients)

By default Winx speaks MCP over stdio - the local transport every desktop client (Claude Desktop, Cursor, VS Code) uses. For clients that live in the cloud and can't reach your machine over stdio - like ChatGPT's developer-mode custom connectors - Winx can also serve MCP over Streamable HTTP:

winx serve --http --bind 127.0.0.1:8000 --token "$(openssl rand -hex 24)"

The MCP protocol is served at /mcp. Every request must carry the token in the Authorization: Bearer <token> header (header-only - a ?token= query parameter would leak the secret into proxy/tunnel access logs and browser history). Without a token the server refuses to start - serving a shell over the network without auth is remote code execution waiting to happen. The endpoint also caps request bodies (64 MB) and times out stuck requests (120 s).

| Flag | Purpose | |------------------|-------------------------------------------------------------------------------------------------| | --http | Serve over Streamable HTTP instead of stdio. | | --bind | Listen address. Defaults to 127.0.0.1:8000. Keep it on loopback. | | --token | Shared secret required on every request. Falls back to the WINX_HTTP_TOKEN env var. | | --allowed-host | Extra Host authority to accept (your tunnel hostname). Repeatable. Loopback is always allowed. |

Remote clients run in the cloud, so the endpoint has to be reachable over HTTPS - put a tunnel in front of the loopback listener and allow its hostname through the built-in DNS-rebinding guard:

# 1. tunnel first, to learn the public hostname
cloudflared tunnel --url http://localhost:8000
#    -> https://<random>.trycloudflare.com

# 2. start Winx, allowing that host
winx serve --http --bind 127.0.0.1:8000 \
     --token "$(openssl rand -hex 24)" \
     --allowed-host <random>.trycloudflare.com

In ChatGPT (Settings → Apps → Advanced → Developer mode), add a connector with:

  • URL: https://<random>.trycloudflare.com/mcp
  • Authentication: bearer / API-key token set to <your-token>, so the connector sends it as

Authorization: Bearer <your-token> (the token is no longer accepted in the URL)

Remote clients are effectively stateless - they don't reuse the MCP session between tool calls - so the HTTP transport shares one shell session across all requests: the shell Initialize creates stays alive for the lifetime of the server, and later BashCommand calls find it. Reuse the same thread_id across calls.

[!WARNING] The HTTP transport puts arbitrary shell and file access on the network. Anyone with the token (and URL) gets a shell on your machine as your user - and not just inside the workspace, since BashCommand in wcgw mode isn't path-restricted. Bind to loopback, keep it behind an authenticated tunnel, prefer architect/code_writer mode or a container, and shut it down when you're done.

Environment variables

All optional - Winx works out of the box without any of these.

| Variable | Effect | |----------|--------| | RUST_LOG | Log verbosity, e.g. winx_code_agent=info. At info you get the per-call audit trail (tool name, arg summary, duration, ok/error). | | WINX_HTTP_TOKEN | Shared secret for the HTTP transport, used if --token isn't passed (see Remote access). | | WINX_NO_COMPRESS | Set to 1 to disable output compression and see raw, uncollapsed shell output (the [winx: ×N] collapsing is on by default). | | WINX_NO_REDACT | Set to 1 to disable secret redaction. By default winx scrubs high-confidence credentials (provider API keys, JWTs, PEM private keys, user:pass@ URLs) from all tool output and saved memory, replacing each with [REDACTED:<rule>]. Turn this off only when you knowingly need a raw value. | | WINX_SANDBOX | Set to 1 to enable an opt-in Landlock filesystem sandbox (Linux 5.13+, EXPERIMENTAL). Confines winx and its shell to write only the workspace (the cwd at startup) plus /tmp, and makes the home directory unreadable, so a manipulated agent can't read ~/.ssh/~/.aws or modify files outside the project. Coarse and best-effort: a command needing a path outside the allowlist fails. Degrades to a warning (unsandboxed) on older kernels. | | WINX_SANDBOX_RO_PATHS / WINX_SANDBOX_RW_PATHS | :-separated absolute paths to additionally allow read-only / read-write under WINX_SANDBOX (e.g. WINX_SANDBOX_RO_PATHS=$HOME/.cargo:$HOME/.rustup so cargo still works). | | WINX_TURN_RECOGNIZER_CONFIG | JSON {"busy":[…],"awaiting_input":[…],"awaiting_approval":[…]} of marker strings/regexes. With recognizer:"configurable", lets wait_for_turn drive an arbitrary TUI without bespoke code. | | WINX_CODING_TOKEN_BUDGET / WINX_NONCODING_TOKEN_BUDGET | Override the per-file token budget for ReadFiles (and saved memory) - raise it for large-context models. Defaults: 24000 / 8000. | | WINX_KEEP_TAIL_PIPE | Set to 1 to keep a trailing \| tail … instead of stripping it. Winx truncates output server-side, so by default it drops a redundant trailing tail (wcgw parity). | | WINX_USE_SCREEN / WINX_ATTACH_TERMINAL | Run the shell inside screen/tmux so you can attach to the live session. Set to screen, tmux, or any truthy value; Winx prints an attach hint on Initialize. | | WINX_OPEN_CONTEXT | Set to 1 to open the saved context file in your default app after ContextSave. | | WINX_SHELL | Set to zsh to run the session under zsh instead of bash (opt-in; bash stays the default). Falls back to bash if zsh isn't on PATH or the mode is restricted. | | WINX_SERVER_INSTRUCTIONS | Extra operator instructions appended to every Initialize response (e.g. house rules for the agent). |

Hacking on it

cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features

CI runs the same three. If you touch src/state/pty.rs or anything in src/tools/bash_command.rs, the regression suite at tests/bash_pty_regression_test.rs is what protects against the usual TUI/PTY foot-guns - run it first.

Robustness is also fuzzed and model-checked:

  • proptest feeds arbitrary/adversarial bytes into the live terminal emulator, the ANSI stripper, and the exit-code

parser, asserting they never panic and stay within the viewport. (This is how we found - and worked around - a vt100 underflow on tiny grids and a reflow panic on column shrink that would otherwise crash the panic = "abort" release.)

  • loom exhaustively model-checks the session pin counter (the lock-free guard that keeps an in-flight session from

being LRU-evicted) across every thread interleaving. It's behind a feature so it doesn't perturb the normal build:

  cargo test --features loom --lib loom_

A note on security

By default this is a local (stdio) MCP server. Anything connected to it can read files, edit files, and run shell commands inside the workspace - same blast radius as letting the model into your terminal. The optional HTTP transport (--http) extends that reach to the network; see Remote access for the extra precautions it demands.

Two things are on by default to reduce the blast radius: secret redaction scrubs high-confidence credentials from all tool output and saved memory (WINX_NO_REDACT=1 to disable), and the PTY shell's whole process group is killed on teardown so background jobs it spawned don't leak.

If you want a tighter leash:

  • architect mode disables writes and most commands;
  • code_writer mode lets you allowlist commands and write globs;
  • WINX_SANDBOX=1 enables an opt-in Landlock filesystem sandbox (Linux): writes are confined to the workspace

plus /tmp, and the home directory is unreadable, so a manipulated agent can't read ~/.ssh/~/.aws or modify files outside the project.

SECURITY.md has the disclosure process and threat model.

License

MIT - Gabriel Maia (@gabrielmaialva33)

Related MCP servers

Browse all →