fx-ruby
August 21, 2026 · View on GitHub
fx-ruby
A tiny, embeddable coding agent for the terminal. Ruby port of fx.

Real CLI, real tools, real permission gate. The model's replies are scripted against a local stub so the recording stays reproducible.
Status: experimental. Built to understand fx by rebuilding it. If you want a coding agent for daily work, use the original.
What is this?
fx-ruby is a coding agent you run in a terminal: it reads your files, edits them, runs commands, and asks before doing anything it cannot undo. It is a port of fx, the Zig agent from Vercel Labs, carrying over the system prompt, tool contracts, permission model, and session format.
It runs on the Ruby standard library alone. No gems to install, nothing to compile, one executable.
Quick Start
git clone https://github.com/jagenaujagenau/fx-ruby.git
cd fx-ruby
./bin/fx setup # store an AI Gateway API key
./bin/fx # start the interactive shell
Requires Ruby 4.0 or newer. There is nothing to install.
fx # interactive shell in the current directory
fx ask "explain the build" # one request, then exit
fx ask "..." --json # machine-readable result on stdout
fx sessions # saved sessions for this workspace
fx session resume last # continue where you left off
fx status # configuration and credential state
fx doctor # check that fx can run here
Credentials come from FX_API_KEY (or AI_GATEWAY_API_KEY) if set, otherwise from ~/.fx/auth.json. Inside the shell, /help lists the interactive commands: /model, /models, /mode, /permissions, /status, /sessions, /clear, /exit.
How it works
An agent turn is a loop: send the conversation, run whatever tools the model asked for, append the results, repeat until it answers. Everything interesting is in what sits between the model and your disk.
graph LR
CLI[bin/fx] --> App[App]
App --> Runtime[Agent::Runtime]
Runtime --> Gateway[Gateway::Client]
Runtime --> Registry[Tools::Registry]
Registry --> Gate{Permissions::Engine}
Gate -->|granted| Tool[15 built-in tools]
Gate -->|denied| Runtime
Tool --> Workspace[(Workspace)]
Runtime --> Session[(Session log)]
The permission engine runs at execution time, not advertisement time: the model always sees every tool, and the gate decides what actually happens.
Permissions
fx starts in auto mode. Recognized development actions run directly; anything else asks first.
| Mode | Behavior |
|---|---|
auto | Known-reversible actions run; sensitive ones prompt |
ask | Every change prompts |
yolo | Nothing prompts. Permissions and sandboxing disabled |
What counts as "known reversible" is a strict allow-list ported from fx's command_effect.zig: git status, npm test, zig build, and similar. Pipes, redirection, shell expansion, global installs, and anything unrecognized go to the approval path. A gap in an allow-list costs an extra prompt; a gap in a deny-list costs you your files.
Approvals can be granted once, remembered as a rule, or refused permanently. Stored rules live in ~/.fx/permissions.json with stable ids, so /permissions revoke <id> works even after files move. Reaching outside the workspace always requires approval, including for reads.
Tools
| Area | Tools |
|---|---|
| Inspection | read_file list_files glob_files grep_files semantic_search file_info |
| Mutation | write_file edit_file create_folder rename_file copy_file delete_file |
| Execution | terminal (exec plus durable background sessions) |
| Other | memory web_fetch |
Every result is bounded and says when it was cut. read_file returns line-numbered output with truncation sentinels, edit_file refuses ambiguous matches instead of guessing, delete_file refuses non-empty directories, and web_fetch rejects private and loopback addresses on every redirect hop.
Configuration
~/.fx/settings.json, resolved in layers. Defaults, then profile, then a per-workspace block, with environment variables winning over all of them.
{
"model": "anthropic/claude-sonnet-4.5",
"permission_mode": "auto",
"statusLine": { "workspace": true },
"workspaces": {
"/path/to/project": { "model": "anthropic/claude-opus-4.5" }
}
}
FX_MODEL, FX_PERMISSION_MODE, FX_MAX_STEPS, and FX_HOME override at runtime.
Sessions
Each session is an append-only JSONL file under ~/.fx/sessions. A crash or an interrupt leaves everything up to that moment readable, and resuming replays the file. A torn final line from an interrupted write is skipped rather than failing the resume.
Embedding
Fx::App is the only place collaborators are constructed, which is the seam for embedding. Supply your own transport, approval handling, or renderer:
app = Fx::App.new(
workspace_root: "/path/to/project",
client: MyTransport.new, # anything answering to #stream
prompter: MyApprovalUI.new, # anything answering to #ask
renderer: Fx::UI::Renderer.new(out: io)
)
turn = app.runtime.run("summarize the recent changes")
puts turn.text
Fx::Agent::Runtime accepts any observer implementing the Fx::Agent::NullObserver interface, which is how the shell renders progress and how tests assert on it.
Project Structure
fx-ruby/
├── assets/
│ └── fx-demo.gif The recording above
├── benchmarks/
│ ├── README.md Methodology and results
│ ├── cli.sh Zig vs Ruby CLI latency (hyperfine)
│ └── micro.rb In-process timings
├── bin/
│ └── fx Executable
├── lib/
│ └── fx/
│ ├── agent/ Runtime loop, system prompt, turn context
│ ├── core/ Workspace, settings, auth, command classifier
│ ├── gateway/ AI Gateway client and stream assembly
│ ├── permissions/ Modes, rules, approval prompts
│ ├── session/ Append-only JSONL persistence
│ ├── tools/ The 15 built-in tools
│ └── ui/ Renderer, shell, slash commands
├── test/ 154 tests, minitest
├── LICENSE
├── README.md
├── Rakefile
└── fx.gemspec
Development
brew install ruby # 4.0.6; the version is pinned in .ruby-version
rake test
154 tests covering the tool contracts, permission decisions, agent loop, streaming assembly, session persistence, and CLI behavior. They run against throwaway workspaces and a throwaway FX_HOME, so they never touch real credentials or sessions. The suite runs clean under ruby -w.
Values that flow through the agent loop, including completions, tool calls, stream events, resolved paths, and tool results, are immutable Data types. The two genuinely mutable types stay Struct: the tool context, whose session is attached after construction, and a terminal session, which is a live process handle.
Performance
Benchmarked against the Zig implementation with hyperfine and profiled in process. See benchmarks/README.md for the full tables and methodology.
| Operation | Before | After | Gain |
|---|---|---|---|
| Build system message | 24.1 ms | 35.6 µs | 677× |
| Tokenize a command | 115.6 µs | 12.9 µs | 9.0× |
fx help (end to end) | 125.0 ms | 57.9 ms | 2.2× |
| grep, 300 files | 62.2 ms | 33.0 ms | 1.9× |
The system message was spawning three git subprocesses per model request. Startup was loading net/http for commands that never open a socket. Neither was a Ruby problem.
Against a 50.9 ms bare-interpreter floor, fx help now spends about 7 ms on its own work. That floor is the wall Ruby cannot get past, and on I/O-bound commands like fx status the gap to the Zig build closes to 1.21×.
Documentation
| Resource | Description |
|---|---|
| benchmarks/README.md | Benchmark methodology, full results, and what each optimization was |
| LICENSE | Apache-2.0 |
| fx.sh | Upstream fx, its docs, and the real thing |
Scope
Ported: the agent runtime, tool suite, permission engine, command classifier, session store, layered configuration, gateway client, interactive shell, and CLI.
Not ported: the WebAssembly and N-API embedding targets, the ACP server, MCP client support, subagents, skills, hooks, and the terminal monitor's condition system. Those are additive surfaces around the core rather than parts of it.
Contributing
Issues and pull requests are welcome. Run rake test before opening one; the suite is fast and covers the parts that matter.
License
Apache-2.0, matching upstream fx.
fx is a project of Vercel Labs. This is an independent port, built to learn from it.