A fork of warpdotdev/warp stripped down to just the terminal.
Auto-update is gone, the AI/agent surfaces are gone, telemetry never runs, and all user state lives next to the binary so the build is genuinely portable. The one thing it adds is saving and streaming output to files, in place of the cloud sharing it removed.
Upstream licensing is unchanged: warpui_core and warpui are MIT
(LICENSE-MIT); everything else is AGPL v3 (LICENSE-AGPL).
| Area | Effect |
|---|---|
| Auto-update | No version checks, no download/install code, no update UI, no "out of date" banner |
| AI / agents | Permanently disabled at the gate; all AI UI and settings pages removed |
| Onboarding | First launch goes straight to a terminal — no welcome slides, no sign-in/skip screen |
| Telemetry | Never initialises; no RudderStack traffic and no retry loop |
| Settings | 9 sidebar entries removed (plus MCP servers, reachable only from the Agents group); 6 remain |
| Shell bootstrap | Works even when the bootstrap script is unsigned or carries Mark-of-the-Web |
| Storage | Portable — everything beside the binary in data\ |
| Version | Tagged ….terminal_NN instead of ….stable_NN |
| File capture | Cloud "Share block/session" replaced by six commands that save or live-stream output to a local file |
| Tool panel | Agent conversations and Warp Drive tabs removed; a Session history tab added in their place |
Only the warp-oss binary is buildable from this tree. The stable, preview,
dev and local binaries call warp_channel_config::load_config!, which shells out
to an internal warp-channel-config generator that is not part of the open-source
repo. warp-oss is the one entry point with a fully hardcoded ChannelConfig
(app/src/bin/oss.rs), and it already ships telemetry_config: None,
crash_reporting_config: None and autoupdate_config: None.
Deleted from app/src/autoupdate/: channel_versions.rs and changelog.rs
(the only two network callers), mac.rs, linux.rs, windows.rs (all
download/install code), plus mod_tests.rs, linux_tests.rs, windows_tests.rs.
app/src/autoupdate/mod.rs is now an inert stub keeping only the type surface the
rest of the app references. get_update_state() always returns
AutoupdateStage::NoUpdateAvailable, start_polling() is a no-op, and
apply_pending_update() always returns false so shutdown proceeds normally.
UI removed from app/src/workspace/view.rs:
- the tab-bar overflow menu that hosted Current version is… / Install update / Update Warp manually (this was the window-caption button)
- the avatar-menu update entries and the avatar's red notification dot
- the tab-bar "Update Warp" pill
render_autoupdate_banner_element()→ returnsNoneunconditionally. This is the function that produced the non-dismissible "Your app is out of date and some features may not work as expected" banner, driven by the server'ssoft_cutoff.- the
AutoupdateState_UpdateReadykeybinding context flag - constants
TAB_BAR_PILL_WIDTH,PILL_FONT_SIZE,UPDATE_READY_TEXT,VERSION_DEPRECATION_BANNER_TEXT,VERSION_DEPRECATION_WITHOUT_PERMISSIONS_BANNER_TEXT
Also: app/src/settings_view/main_page.rs — the version widget's Relaunch /
DownloadUpdate / CheckForUpdate actions are no-ops.
app/src/changelog_model.rs — the changelog fetch is gone; it hit the same release
server.
The TUI updater (
crates/warp_tui/src/autoupdate.rs) is untouched — it belongs to a separate binary that this build does not produce.
app/src/settings/ai.rs — AISettings::is_any_ai_enabled() returns a constant
false, and the backing setting's default flipped to false for the few call sites
that read the field directly.
This one gate is consulted by ~200 UI sites and is projected into the keybinding
context system as flags::IS_ANY_AI_ENABLED, so every AI command-palette entry and
keybinding predicated on that flag disappears with it.
The AI code is still compiled, just unreachable. app/src/ai alone is ~540 files;
deleting it would cascade through workspace/view.rs (~28k lines), the settings-view
event enum and lib.rs startup. That was judged not worth the risk.
Each page's should_render() returns false, which removes it from both the sidebar
and the content area through the single choke point
SettingsView::filtered_pages() in app/src/settings_view/mod.rs — so deep links
(warp://settings) and search cannot reach them either.
| Page | File |
|---|---|
| Account | main_page.rs |
| Agents (umbrella) | ai_page.rs |
| Code (umbrella) | code_page.rs |
| Cloud platform (umbrella) | environments_page.rs, platform_page.rs |
| MCP servers | mcp_servers_page.rs |
| Referrals | referrals_page.rs |
| Warp Drive | warp_drive_page.rs |
| Teams | teams_page.rs |
| Billing and usage | billing_and_usage_dispatch.rs |
| Shared blocks | show_blocks_view.rs |
In app/src/settings_view/mod.rs the nav_items list was trimmed to match, the
Scripting insert was re-anchored (it positioned itself relative to the now-absent
SharedBlocks), and #[default] moved from Account to Appearance — Account
was the default landing section and would otherwise have opened a blank pane.
Remaining: Appearance, Features, Keyboard shortcuts, Warpify, Privacy, About.
user_menu_items() in app/src/workspace/view.rs now offers only Settings,
Keyboard shortcuts, Documentation and View Warp logs.
Removed: What's new, Feedback, Join our Slack community, Sign up, Upgrade, Billing and usage, Invite a friend, Log out.
The command-palette mirrors of those entries were removed too, from
add_overflow_menu_items_as_editable_binding() in app/src/workspace/mod.rs —
otherwise they stayed reachable by typing.
app/src/root_view.rs — auth_onboarding_state resolves to
AuthOnboardingState::Terminal unconditionally on native builds. This bypasses the
agent onboarding slides, the post-onboarding sign-in / skip screen
(app/src/auth/login_slide.rs) and the Auth screen.
Side effect worth knowing: onboarding was also what wrote
default_session_mode = "agent" and a full agent execution-profile block into
settings.toml. Skipping it means a fresh profile contains only appearance defaults,
and new tabs open as terminals.
app/src/server/telemetry/mod.rs — send_batch_messages_to_rudder() returns early
when ChannelState::is_telemetry_available() is false.
Nothing was ever transmitted before either: the OSS channel ships
telemetry_config: None, so the write key and root URL are empty and every request
failed in the HTTP builder. This removes the pointless retry loop, which was logging
builder error roughly every 30 seconds.
app/assets/bundled/bootstrap/pwsh_init_shell.ps1 now sets process-scope Bypass
whenever MachinePolicy and UserPolicy are not Restricted. Previously it only
relaxed the policy when the effective policy was Restricted.
Why this matters: Warp's released pwsh.ps1 is Authenticode-signed; a self-built one
is not. If the source tree came from a downloaded archive it also carries
Mark-of-the-Web. Under RemoteSigned that combination makes PowerShell refuse to
dot-source the bootstrap, and the terminal hangs forever on
"Starting PowerShell Core…" with the error only visible on the shell's stderr,
never in warp-oss.log.
Process scope only — nothing is written to machine or user policy, and Group Policy still wins if it is set.
crates/warp_core/src/paths.rs adds portable_root(), using the convention VS Code
uses: if a directory named data sits next to the executable, all user state goes
there. Opt-in and reversible — delete the folder and the app falls straight back to
the per-user profile, which is left untouched.
Redirected: config_local_dir(), data_dir(), state_dir(), cache_dir(),
gui_config_local_dir(), warp_home_config_dir(). Everything else
(themes_dir(), tui_config_local_dir(), tui_state_dir(), the log directory)
derives from those.
warp-oss.exe
data\
config\ settings.toml, keybindings.yaml, user_preferences.json, cli\
state\ warp.sqlite, logs\, telemetry queue, index snapshots
cache\
.warp\ workflows, skills, .mcp.json
Roaming and local are collapsed into one state\ — a portable install has no roaming
profile to sync with. Upstream splits these across two roots
(%LOCALAPPDATA% and %APPDATA%), which is easy to miss.
Cargo.toml — [profile.rlto] gained strip = "symbols". Note this buys almost
nothing on Windows/MSVC, where debug info goes to a separate .pdb rather than into
the .exe; it is kept for ELF/Mach-O targets.
The version string is set at build time via GIT_RELEASE_TAG (read through
option_env! in crates/warp_core/src/channel/state.rs) — no code change. The
channel segment is capture group 3 of the parser's regex in
crates/channel_versions/src/lib.rs, and ParsedVersion discards it, so
.terminal_01 parses and orders identically to .stable_01.
app/src/themes/default_themes.rs — dark_theme() background restored to
0x000000FF. Upstream changed it to 0x050505FF, which looks washed out on OLED.
The one feature this fork adds. It replaces Share block... and Share session...,
which uploaded to Warp's cloud and are therefore dead here, with six commands that
write the same content to a local file.
| Command | Binding name | Writes | Keybinding |
|---|---|---|---|
| Save output to file... | Save Output | The block's output, as rendered | Ctrl+Shift+S |
| Stream output to file... | Stream Output | The same, then keeps appending while the command runs | Ctrl+Alt+Shift+S |
| Save block to file... | Save Block | Command line and output | unbound |
| Stream block to file... | Stream Block | The same, live (only offered while a command is running) | unbound |
| Save session to file... | Save Session | Every block in the session | unbound |
| Stream session to file... | Stream Session | The same, then follows the session across block boundaries | unbound |
The two keybindings act on the selected block(s) if there is a selection, otherwise on
the last block. Stream degrades to Save when nothing is running. With more than one
block selected the labels pluralise and the session commands are hidden.
All six are registered as editable bindings, so they appear in the Terminal section
of the Resource Center's keyboard shortcuts panel and in Settings → Keyboard
Shortcuts, where the four unbound ones can be given a key. No default chords were
invented for them — the modifier space around Ctrl+Shift+S is already crowded.
New: app/src/terminal/file_capture.rs — scopes, filename-pattern expansion and
FileStream, which appends when the rendered text still starts with what it last wrote
and rewrites from the recorded offset when it does not (a block's tail can be redrawn in
place). app/src/terminal/file_capture_tests.rs covers it with 13 tests.
Changed:
app/src/terminal/view.rs— the six actions, the wakeup tick that pumps live streams, andsession_capture_context_menu_items, one builder shared by all four menus that used to offer session sharing (block right-click, the block "..." button, the input-area right-click and the empty-session menu).app/src/terminal/model/block.rs—command_and_output_with_secrets_unobfuscated()andcommand_line_with_pwd(), which prefixes the command with the directory it ran in, prompt-style, using each block's own recorded pwd and shell type.app/src/tab.rsandapp/src/workspace/view.rs— the same two session commands on the tab right-click menu, resolving the tab by index so saving another tab's session does not steal focus.app/src/terminal/settings.rs— threeStringsettings,SyncToCloud::Never.app/src/settings_view/features_page.rs— those three settings under Settings → Features → Session, one text field each.app/src/terminal/view/init.rs— all six registered as editable bindings, namedterminal:{save,stream}_{output,block,session}_to_file.app/src/resource_center/utils.rs— the six names added toTERMINAL_KEYBINDINGSso they land in the panel's Terminal section instead of Fundamentals.app/src/resource_center/keybindings_page.rs—build_bindingsdropped every binding without a trigger, which would have hidden the four unbound captures. It now keeps those six by name.
| toml key | Default |
|---|---|
terminal.file_capture.output_pattern |
{command}_{timestamp}.log |
terminal.file_capture.block_pattern |
{command}_{timestamp}.log |
terminal.file_capture.session_pattern |
{session-or-command}_{timestamp}.log |
Tokens: {command} (first block's command), {timestamp} (now),
{finished-timestamp} (last block's finish time), {session-or-command} (the tab name
if you renamed it, otherwise the first command). Values are sanitised for the
filesystem, and the expansion only seeds the save dialog — the final name is yours.
Clearing a field in the settings page restores its default.
Captured files contain secrets in plaintext. The terminal masks them on screen; these files do not. That is deliberate — the point is a faithful transcript — but treat the output as sensitive.
The tool panel (left panel) had four tabs. The two cloud/AI ones are gone and a new one takes their place:
| Tab | Shortcut | Status |
|---|---|---|
| Project explorer | Alt+1 |
unchanged |
| Session history | Alt+2 |
new |
| Global search | Alt+3 |
unchanged |
Alt+2 |
removed | |
Alt+4 |
removed |
Session history lists the commands run in the active session. Each row is three lines — the command, its working directory, then how long it took, its exit code if it failed, and how much output it produced:
cargo build --release
~/projects/app
1m 04s · 612 lines · 38.6 KB
cargo test
~/projects/app
22.1s · exit 101 · 88 lines · 4.2 KB
Failed commands get the same treatment the block list gives a failed block: a red left stripe over a 10% wash. Running commands appear immediately with a ticking duration and growing size. Rows are in terminal order, oldest first.
Exit codes are shown only for commands that failed, in decimal. On Windows a
code of 256 or more also gets its hexadecimal form —
exit -1073741819 (0xC0000005) — because anything past the 0-255 range a shell
can report is a Windows status code, which documentation and search results
give in hex. The decimal stays signed while the hex is the unsigned
reinterpretation, since those status codes overflow i32. The cutoff is
EXIT_CODE_HEX_THRESHOLD in app/src/terminal/session_history.rs.
Selection is shared with the block list: select in either and the other highlights and scrolls to match. Rows use the block list's own modifier rules — Shift-click extends a range, Ctrl-click toggles one in or out, a plain click selects one. Double-click hands the keyboard to the terminal.
Copy and capture shortcuts work while the panel is focused: Ctrl+Shift+C,
Ctrl+Alt+Shift+C, Ctrl+Shift+S and Ctrl+Alt+Shift+S, acting on the shared
selection. They are registered under the terminal's own binding names, so
Settings → Keyboard Shortcuts shows one row per command, not two.
The query field holds the regex and case-sensitive toggles and a clear button, laid out like the find bar and the block filter. Below it are five toggles in two groups:
- scope — command, working directory, output. Independent; command and directory start on, output starts off because it is the only one that has to read every block's text. With it on, rows show a match count.
- on-select — what happens when you select a row:
| filter-on-select | find-on-select | behaviour |
|---|---|---|
| off | off | scroll to the block only |
| on | off | open the block filter toolbar, pre-filled |
| off | on | open the find bar, scoped to that block |
| on | on | apply the filter silently, and open the find bar |
These fire only for a plain click in the panel. A modified click is building a selection, and a selection made in the terminal must not silently filter the output of a block you merely clicked on there.
New: app/src/terminal/session_history.rs (the row snapshot type, output
size measurement, and the duration/byte formatting, with unit tests in
session_history_tests.rs) and app/src/workspace/view/session_history.rs
(the panel itself).
Changed:
app/src/workspace/view.rs—compute_left_panel_viewsno longer offers the two removed tabs; restoring a session onto one falls back to Project explorer.app/src/workspace/view/left_panel.rs— theSessionHistorytab, its toolbelt button (Icon::History) and its render/focus wiring.app/src/terminal/view.rs— the panel-facing API:session_history_entries,block_output_size,reveal_block_from_session_history, the filter/find entry points, andselect_block_from_session_history, which shares the block list's modifier rules viaapply_modified_block_selectionso the two cannot drift.app/src/terminal/block_list_element.rs— aSessionHistoryRowmenu source carrying a row-relative position, so the block menu can open over the panel.app/src/workspace/mod.rs— eight bindings in the panel's context, repeating the terminal bindings' names and descriptions so the keybindings page dedupes them into one row each.app/src/settings/code.rs—code.editor.show_session_history.
The ToolPanelView::WarpDrive and ::ConversationListView variants still
exist, and their panel views are still compiled. They are unreachable: keeping
them lets tab state persisted before the removal still deserialize instead of
being discarded.
Compiler warnings introduced by the removals were cleaned up: unused imports and
variables removed, five dead constants deleted, and genuinely unreachable code
(the changelog parsing methods, the settings-umbrella machinery) annotated with
#[allow(dead_code)] and a comment explaining why. app/src builds warning-free.
New files: script/windows/portable/install.ps1 and uninstall.ps1 — optional
convenience only (Start Menu shortcut, Unblock-File, optional PATH entry). The
portable folder runs without them.
Two scripts wrap everything below. Prefer them — the flags are easy to get wrong, and getting them wrong produces a binary that looks fine on the build machine and is broken everywhere else.
.\script\windows\build-portable.ps1 # derives the next tag and builds
.\script\windows\package-release.ps1 # zips it, hashes it, writes notes
.\script\windows\package-release.ps1 -Build # both in one stepbuild-portable.ps1 refuses to finish unless the linked binary reports the tag it
was asked for and links as PE subsystem 2, which is the only direct evidence that
release_bundle applied. package-release.ps1 takes the version from the exe
rather than a parameter, so an archive cannot claim a version the binary does not
have; it audits the staged folder for user data before archiving, and asks
claude to write the release notes from the commits since the previous release
tag. Neither script creates a tag or pushes anything.
The rest of this section is what those scripts do, and why.
Prerequisites: Rust 1.98.1 (pinned by rust-toolchain.toml), VS Build Tools with
the MSVC toolchain and a Windows SDK, CMake, protoc, NASM. LLVM/libclang is not
needed — bindgen only runs when the target OS is macOS
(crates/warpui/build.rs).
$env:CARGO_FULL_PROFILE = 'rlto' # app/build.rs stages the Windows DLLs here
$env:CARGO_BIN_NAME = 'stable' # picks app\channels\<name>\icon -> the exe icon
$env:WARP_APP_NAME = 'WarpOss'
$env:GIT_RELEASE_TAG = 'v0.2026.08.08.00.00.terminal_01'
cargo build -p warp --profile rlto --bin warp-oss --features release_bundle,guiDo not pass --target x86_64-pc-windows-msvc. app/build.rs stages conpty.dll,
dxcompiler.dll, dxil.dll and x64\OpenConsole.exe into target\<profile>\, which
does not include the target triple — omitting the flag puts the exe in the same place
and the layout builds itself.
release_bundle is required, not optional: it enables rust-embed's debug-embed,
without which assets are read from absolute build-machine paths and the binary is not
portable. It also sets windows_subsystem = "windows" so no console window flashes.
warp-oss.exe conpty.dll dxcompiler.dll dxil.dll
msvcp140.dll vcruntime140.dll vcruntime140_1.dll
pwsh.ps1 icon.ico x64\OpenConsole.exe
data\ <- presence of this folder enables portable mode
The first four come from target\rlto\ (already staged by build.rs); the VC++
runtime DLLs come from app\assets\windows\x64\. Fonts, themes, images and shell
bootstrap scripts are embedded in the exe — nothing else to copy.
Run Unblock-File over the staged folder if the source tree came from a downloaded
archive, otherwise the exe trips SmartScreen on first run.
script/portable/build-portable.sh # derives the next tag and builds
script/portable/package-release.sh # stages and archives it
script/portable/package-release.sh --build # both in one stepSame contract as the Windows pair: the build script owns the flags and verifies them
against the linked binary, and the package script reads the tag out of the binary
rather than taking it as an argument. Artifacts are
warp-terminal-<tag>-linux-x64.tar.gz and warp-terminal-<tag>-macos-arm64.tar.gz,
each holding the binary, a data/ marker, and on Linux an optional install.sh
that adds a desktop entry and icon under $HOME.
Three differences from the Windows build, all deliberate:
- No
guifeature.gui = ["voice_input"]does not gate the GUI front-end, so a terminal-only build has no use for it, and leaving it out keeps ALSA off the runtime dependency list.release_bundleis still required, fordebug-embed. CARGO_PROFILE_RLTO_DEBUG=0. ThinLTO with debuginfo needs more than 16 GB and the finalrustcgets OOM-killed — on a 16 GB VM and on GitHub's runners alike. Line tables are the dominant consumer. The cost is that panics on these platforms carry addresses rather thanfile:line; Windows keepsdebug = 1.- macOS binaries are ad-hoc codesigned (
codesign --sign -). An unsigned arm64 Mach-O is killed by the kernel, so this is required rather than cosmetic. Only the binary is signed and no bundle resources are sealed, so the app writing into a siblingdata/at runtime cannot invalidate the signature. No.appis produced:app/src/bin/oss.rsembeds its ownInfo.plistthroughembed_plist, so the bare binary already carriesCFBundleIdentifierand the rest.
The binary dlopens its windowing and graphics libraries rather than linking them, so
ldd lists only seven entries and a missing one shows up as a failure to open a
window rather than as a load error. On a minimal Ubuntu install:
sudo apt install libx11-6 libx11-xcb1 libxcb1 libxi6 libxcursor1 libxkbcommon0 libxkbcommon-x11-0 libwayland-client0 libwayland-egl1 libegl1 libvulkan1 mesa-vulkan-drivers fontconfigA Vulkan driver is needed as well as the loader — a present libvulkan1 with nothing
behind it fails the same way. script/portable/install.sh checks for all of these and
prints the apt line for whatever is missing, so it is worth running once even if you
do not want the desktop entry.
Linux build dependencies come from script/linux/install_build_deps. If the checkout
sits on a network or VM share, set CARGO_TARGET_DIR to a local path first — building
into a share is slow, and if that share is also a Windows checkout it mixes two
platforms' artifacts in one profile directory.
.github/workflows/build-portable.yml runs both on ubuntu-24.04 and macos-14, on
a pushed v0.*.terminal_* tag or on manual dispatch. Windows is not in CI: the local
PowerShell script already does it in about twelve minutes with a warm cache.
Building on Ubuntu 24.04 rather than 26.04 is deliberate — the older glibc means the artifact also runs on newer distributions, which the reverse would not.
- Session history is per-session. It reads the live block list, so it does not
survive a restart, it swaps when you change tab, and blocks trimmed by
terminal.maximum_grid_sizedrop out of it. The sqlitecommandstable would persist across restarts but stores no output size, which is one of the columns the tab shows. - Output-scope filtering is the slow one. Matching against command and directory is free; matching against output has to read every block's text. It is off by default for that reason.
- Not zero-footprint.
app/src/app_services/windows/mod.rscallsregister_uri_handler()at startup, writing thewarposs://handler toHKCU\Software\Classes. Per-user, but outside the portable folder. - The Resource Center panel still has community entries — its footer offers Join our Slack community and Feedback, and its main page has an Invite a friend to Warp card that dispatches to the (now removed) referrals page.
- The binary is ~350 MB. Roughly 61 MB is embedded assets; the rest is compiled
code, including the unreachable AI stack. Deleting
app/src/aiis the only real lever. - Unsigned. SmartScreen will warn on first run of a copy that carries Mark-of-the-Web.
- Upstream warnings remain in
crates/warpui_core(8) andcrates/warp_terminal(1). They predate this fork and were left alone.