Overview

How it works

Write a command-line app on your server. Terminalwire streams it to a tiny client your users install once. It's like a browser, but for the terminal.

app/cli/blog.rb
class BlogCLI < Terminalwire::CLI  desc "posts", "List your posts"  def posts    current_user.posts.each { puts _1.title }  end  desc "login", "Sign in"  def login    browser.launch login_url    puts "signed in as #{current_user.email}"  endend
Your web app
WS
blog · their terminal
blog.dev/login
Sign in to blog

Authorize your terminal

ada@blog.dev
Continue

then back to your terminal

Their terminal

No API to build· No client to ship· No updates to push

One wire, the whole terminal

You just saw a command print and open a browser. The same round trip reaches their files, their environment, and whatever they pipe in. Every bit of it is gated by the user's grant.

Read what they pipe in

Pipe a file (or a whole folder) straight into a command. The bytes stream up to your server and your code reads them from stdin. No prompts, because the user chose what to send.

app/cli/blog.rb
desc "import", "Import posts piped in"def import  Post.import_markdown stdin.read  puts "imported #{Post.count} posts"end
Your web app
WS
blog · their terminal
Their terminal

Write a file, only if they allow it

Files are gated like a browser download. The first write is denied; your code rescues it and prints the exact grant command. The user runs it once, then the write goes through.

app/cli/blog.rb
desc "export", "Save posts to a file"def export  file.write "posts.csv", current_user.posts.to_csvrescue Terminalwire::Denied  puts "Run: terminalwire-exec policy grant ./posts.csv"end
Your web app
WS
blog · their terminal
Their terminal

Read an environment variable

Secrets stay on the user's machine until they allow them. Your CLI asks for the variable; the first run prints the entitlement to grant, and after that the server reads it on every run.

app/cli/blog.rb
desc "deploy", "Deploy with your token"def deploy  Deploy.run! token: env.fetch("BLOG_TOKEN")rescue Terminalwire::Denied  puts "Run: terminalwire-exec policy grant --env BLOG_TOKEN"end
Your web app
WS
blog · their terminal
Their terminal

Pipe something in, preview it back

Pipe anything to a command. The bytes stream up to your server, your code renders them, and --preview pops the result open in their browser.

app/cli/blog.rb
desc "post", "Create a post from stdin"def post  body = stdin.read  preview = render_preview(body)  browser.launch preview.urlend
Your web app
WS
blog · their terminal
blog.dev/preview/draft
Draft preview
Hello, world
Their terminal

Install & updates

Write it once. It runs on every OS.

You build your CLI once, on your server, and it runs on macOS, Linux, and Windows. There's no installer to write, nothing to code-sign, and no software updates to push. None of it is your problem.

Terminalwire hosts the one-line install at blog.terminalwire.sh and keeps the client current on all three platforms. After that, the client talks straight to your server and streams your latest CLI on every run, so everyone is always on the current version. You write commands. We handle distribution.

macOS Linux Windows
blog.terminalwire.sh
# macOS & Linux
$ curl blog.terminalwire.sh | sh
  installing the blog client…
 installed blog

# Windows (PowerShell)
> irm blog.terminalwire.sh | iex
 installed blog
Hosted & kept up to date by Terminalwire

Watch a real one ship

The hard questions

Your security team will have questions. The answers are written down.

The server is open source and runs in your infrastructure. The client is sandboxed like a browser, and updates only run when they verify against our offline root key.