On March 27, 2026, sh0 went from a deployment platform with a basic CLI to a deployment platform with a complete developer experience. Sixteen new commands. Two new server endpoints. A WebSocket streaming system. Documentation across three surfaces in five languages. Six independent audit sessions. Zero known bugs at the end.
This article tells the complete story -- not the implementation details (those are in the preceding articles) but the methodology, the timeline, the decisions, and the lessons learned.
The Starting Point
sh0 already had a CLI. Ten commands built on day one of the project, covering the essentials: serve, setup, status, deploy, logs, env, ssh, check, templates, compose, and more. These commands mirrored the dashboard and were immediately useful for development and testing.
But they all assumed the app already existed on the server. The developer had to create apps through the dashboard, configure Git repositories, and manage deployments through the web UI. The CLI was a remote control for the server, not a self-contained development tool.
The gap was sh0 push -- the ability to deploy from a local directory with a single command.
The Timeline
| Session | Phase | What was done | Findings |
|---|---|---|---|
| 1 | Phase 1: Build | push, login, whoami + server endpoint | -- |
| 2 | Phase 1: Audit R1 | Review 2,600 lines | 3 Critical, 6 Important, 5 Minor |
| 3 | Phase 1: Audit R2 | Verify fixes, fresh review | 2 Important, 4 Minor |
| 4 | Phase 2: Build | init, link, open, config | -- |
| 5 | Phase 2: Audit | Review Phase 2 code | 0 Critical, 1 Important, 2 Minor |
| 6 | Phase 3: Build | restart, stop, start, delete, domains | -- |
| 7 | Phase 3: Audit | Review Phase 3 code | 1 Critical, 0 Important, 4 Minor |
| 8 | Phase 4: Build | watch + WebSocket streaming | -- |
| 9 | Phase 4: Audit | Review Phase 4 code | (included in global audit) |
| 10 | Global Audit R1 | Cross-phase review (3,200 lines) | 2 Critical, 5 Important, 7 Minor |
| 11 | Global Audit R2 | Verify global fixes | 0 new findings |
| 12 | Phase 5: Docs | Marketing, dashboard, 4 doc pages | -- |
| 13 | Phase 5: Update | Sync docs with Phase 2-4 commands | -- |
Thirteen sessions. Each session operates with fresh context -- no carryover from previous sessions, no builder bias.
What Was Built
New CLI Commands (16)
| Command | Lines | Phase | Purpose |
|---|---|---|---|
sh0 push | ~580 | 1 | One-command deploy from local directory |
sh0 login | ~90 | 1 | Interactive authentication |
sh0 whoami | ~30 | 1 | Display current identity |
sh0 init | ~120 | 2 | Stack detection + .sh0ignore generation |
sh0 link | ~60 | 2 | Link directory to existing app |
sh0 open | ~50 | 2 | Open app URL in browser |
sh0 config | ~100 | 2 | Manage config file |
sh0 restart | ~15 | 3 | Restart application |
sh0 stop | ~30 | 3 | Stop with confirmation |
sh0 start | ~15 | 3 | Start stopped application |
sh0 delete | ~40 | 3 | Delete with name-match confirmation |
sh0 domains | ~100 | 3 | Domain management subcommand |
sh0 watch | ~130 | 4 | Auto-push on file changes |
New Server-Side Code
| Endpoint | Purpose |
|---|---|
POST /api/v1/apps/:id/upload | Re-upload to existing app |
GET /api/v1/deployments/:id/stream | WebSocket build log streaming |
Supporting Infrastructure
| Component | Purpose |
|---|---|
Deployment::has_active_by_app_id() | Concurrent deployment guard |
ApiError::Conflict | HTTP 409 response variant |
StreamResult struct | Unified terminal state from WS/HTTP |
update_phase_from_log() | Shared build phase detection |
should_ignore_public() | Shared ignore logic for push/watch |
Documentation
| Surface | Pages/Commands |
|---|---|
Marketing page (sh0.dev/cli) | 31 commands, 5-step workflow |
Dashboard page (/cli) | 29 commands, 5 languages |
| Docs overview | 4 core workflows |
| Docs installation | Platform-specific guide |
| Docs push & deploy | Full push reference |
| Docs commands | Complete command reference |
The Audit Scorecard
| Metric | Phase 1 | Phase 2 | Phase 3 | Global | Total |
|---|---|---|---|---|---|
| Critical | 3 | 0 | 1 | 2 | 6 |
| Important | 8 | 1 | 0 | 5 | 14 |
| Minor | 9 | 2 | 4 | 7 | 22 |
| Fixed | 11 | 1 | 1 | 7 | 20 |
| Tests added | 1 | 0 | 0 | 0 | 1 |
| Regressions | 0 | 0 | 0 | 0 | 0 |
Six Critical findings across 3,200 lines of code. That is one Critical bug per 533 lines. For context, industry studies estimate one security vulnerability per 1,000-2,000 lines of code in production software. The audit process found and fixed issues at roughly twice the typical detection rate.
The zero-regression count is worth noting. Twenty fixes applied across six audit sessions, with no fix introducing a new bug. This suggests that the fixes were surgical (targeted changes to specific functions) rather than structural (refactors that could cascade).
Key Decisions
Decision 1: Audit After Each Phase, Not After All Phases
We could have built all four phases and then audited the complete codebase once. This would have been faster (fewer sessions) but less effective. Bugs found in Phase 1's audit informed the patterns used in Phase 2's implementation. The atomic write pattern, discovered as a bug fix in Phase 1, was applied proactively in Phase 2 and 3.
Decision 2: Global Audit After Per-Phase Audits
Per-phase audits examine code within a boundary. They cannot detect inconsistencies across boundaries. The global audit found cross-cutting issues that no per-phase audit could have caught: token masking inconsistency, ignore logic divergence, pagination limits.
The lesson: local correctness does not imply global correctness. A function can be correct in isolation and still be wrong in context.
Decision 3: Reuse Existing Infrastructure
All 16 commands were built on top of existing sh0 code:
- Stack detection:
sh0_builder::detector::detect_stack() - Health check:
sh0_builder::health::check_health() - Upload pipeline:
sh0-api/src/deploy/pipeline.rs - App resolution:
Sh0Client::resolve_app() - WebSocket client:
tokio-tungstenite(fromsh0 logs) - WebSocket server:
axum::extract::ws(from terminal)
No new dependencies were introduced for server-side code. The only new client-side dependencies were notify (filesystem watcher), percent-encoding (URL encoding), and feature flags on existing crates.
Decision 4: WebSocket with HTTP Fallback
The build log streaming could have been WebSocket-only. We chose to keep HTTP polling as a fallback because:
- Some reverse proxies strip WebSocket upgrade headers
- The CLI must work against sh0 servers that pre-date the stream endpoint
- The fallback is free -- it is the existing Phase 1 code, refactored into a function
The cost of the fallback is ~50 lines of code. The benefit is backwards compatibility with zero breakage.
What We Learned
1. The Builder-Auditor Gap Is Real
The same model that builds the code cannot audit it effectively in the same session. Not because of a capability limitation, but because of a context limitation. The builder has accumulated hundreds of micro-decisions and assumptions. The auditor starts with none.
This is not unique to AI. Human developers have the same blind spots for their own code. The solution is the same: independent review by someone who did not write it.
2. Cross-Phase Audits Are Non-Negotiable
Five of the twenty fixes came from the global audit. These were bugs that existed in the space between commands, invisible to any single-command review. Token masking, ignore logic, pagination limits -- all cross-cutting concerns that require seeing the whole surface at once.
3. Documentation Debt Accumulates Faster Than Technical Debt
Between Phase 1 docs and the Phase 5 update, ten commands were completely undocumented. The gap was approximately six sessions. In a traditional development team, this would be weeks or months of drift. With AI-assisted development, it was hours -- but the debt was the same: features that exist but that no one can discover.
The fix is batch documentation updates after implementation is complete, not incremental documentation during implementation. Incremental docs get stale as subsequent phases change the surface.
4. Thin Wrappers Are Underrated
Phase 3's five commands are each under 50 lines. They are thin wrappers around API calls with confirmation prompts. There is nothing clever about them. They are also the most immediately useful additions to the CLI, because they replace common dashboard actions with single commands.
The lesson: not every feature needs to be technically interesting. Sometimes the highest-value work is the simplest code.
The Complete CLI
sh0 now ships with 30+ commands:
Authentication push, login, whoami, config
Deployment push, init, link, watch, deploy
App Lifecycle restart, stop, start, delete, open
Domains domains list, add, remove
Management status, logs, env, ssh, scale
Infrastructure templates, compose, cron, yaml
Advanced hooks, preview, export, users, projectsEvery action in the dashboard has a CLI equivalent. Every CLI command has documentation. Every piece of code has been audited at least twice. The CLI is not a secondary interface -- it is a first-class citizen of the sh0 platform.
From zero to here in one day. Not because one day is fast, but because the methodology -- build, audit, audit, document -- makes it possible to move quickly without moving recklessly.
The Broader Point
This article series is not about a CLI. It is about a methodology for building production software with AI.
The methodology has three properties:
- Separation of concerns: The builder optimizes for features. The auditor optimizes for correctness. The documenter optimizes for clarity. No single session tries to do all three.
- Fresh context as a feature: Each audit session starts without the builder's assumptions. This is not a limitation of AI context windows -- it is a deliberate design choice that produces better reviews.
- Convergence through diversity: Build, audit, audit, approve. Each pass sees the code from a different angle. The result converges on something no single pass would produce.
Sixteen commands. Six audit sessions. Twenty bug fixes. Zero regressions. One day.
That is what the methodology produces. Not perfection -- but a systematic reduction of the gap between "it works on my machine" and "it works in production."
This article is part of the "How We Added a CLI to sh0" series. Start from the beginning: One Command to Deploy: How We Built sh0 push.