Build Warp

KINDAreplaces $18/mosaves $216/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A block-based terminal whose hard problems are getting command boundaries from the shell instead of guessing at them, treating program output as hostile input to the renderer, and putting a tested redaction boundary in front of the one function that talks to a model.

estimated effort multi-daythe files for this build are in the project pack

RuntimeNode 22 with node-pty, bound to the loopback interface onlyRendererxterm.js in a browser tab served from localhost, with a locked-down sequence policyShell integrationA snippet the user installs that emits the OSC 133 prompt markersStorageSQLite for block metadata, output bodies as files on disk under a retentionAssistantOne request per explicit ask, through a single function that refuses unfiltered textRedactionPattern rules plus a deny list of variable names, with an attack corpus in the suite

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the server, the pty bridge and the build.

    Get it nodejs.org or your version manager. open ↗

    Verify node --version prints v22 or higher

  2. decidefree

    Why The integration snippet is per shell, and supporting three badly is worse than supporting the one you actually use.

    Get it Pick the shell you run daily and write the snippet for its prompt hooks.

    Verify The snippet emits the markers and block rows appear

  3. have readyfree

    Why Phase 2 involves piping deliberately hostile output through the renderer, and you want that happening somewhere you do not mind breaking.

    Get it Any container runtime, or a virtual machine with a snapshot.

    Verify You can run the hostile fixture inside it and discard the result

  4. API keypay per request; the ceiling in this build is what keeps it small

    Why The assistant is the only paid and only networked part of the build, and it is optional by design.

    Get it Create a key with your preferred provider and set a spend limit in their console as well.

    Verify One request returns a completion and writes an outbound row

  5. have readyfree

    Why The redactor is only as good as what you have attacked it with, and real credentials must never be the test data.

    Get it Write fake keys, headers, private key blocks and connection strings by hand into the fixtures directory.

    Verify The suite runs them and reports zero unmasked matches

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- blocks(id, session_id, started_at, ended_at, command, cwd, exit_code, output_path, bytes, truncated) · one row per command, boundaries taken from the shell markers
- sessions(id, shell, started_at, ended_at, tty, env_fingerprint) · env_fingerprint is a hash, never the variables themselves
- rules(id, pattern_kind, pattern, label, enabled) · redaction rules, shipped with defaults and editable by hand
- redactions(id, block_id, rule_id, span_start, span_end, label) · what was masked and by which rule, so a false positive is findable later
- outbound(id, at, kind, block_ids_json, byte_count, sha256, model_id, cost_minor) · one row per request that left the machine
- proposals(id, block_id, command_argv_json, rationale, accepted_at, rejected_at) · a suggested command is a row you can read, never a keystroke pushed into the pty
- secrets_flagged(block_id, at, reason) · blocks excluded from the search index and from any selection offered to the model
- No table here holds a model answer beside the raw output it read. The raw output stays in its block file and the answer is a proposal that points at it.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DATABASE_PATHrequired./data/terminal.dbBlocks, sessions, redactions and the outbound log. Back this up or delete it deliberately.
OUTPUT_DIRrequired./data/outputBlock output bodies on disk. Expired on a schedule, rows retained.
OUTPUT_RETENTION_DAYSrequired14How long output bodies survive. Shorter is the sane default for a laptop.
MAX_BLOCK_BYTESrequired262144Per-block storage cap. Above it the block is truncated visibly.
REDACTION_DENY_VARSrequiredAWS_SECRET_ACCESS_KEY,DATABASE_URL,GITHUB_TOKENVariable names whose values are masked wherever they appear in output.
MODEL_API_KEYsecretoptionalsk-...redactedAbsent by default. Without it the assistant panel is not shown.
MODEL_IDoptionala text model identifierRecorded on every outbound row, because answers differ by model.
MONTHLY_COST_CEILINGrequired5.00The assistant stops at this figure and the terminal carries on.

The build, in order

  1. A real pseudoterminal, and blocks the shell tells you about

    Commands, working directories and exit codes come from the shell rather than from pattern matching on the screen.

    1. A shell attached to a pipe disables its prompt, its job control and its colour.

      Files src/pty.tssrc/server.ts

      terminal
      npm i node-pty @xterm/xterm ws
    2. The shell knows where a command starts and the screen does not.

      Files shell/integration.shsrc/blocks.tstests/blocks.test.ts

    3. Silently guessing is worse than visibly not knowing.

      Files src/blocks.tssrc/ui/Banner.tsx

    done when · tick each as it passes
    watch out
    • Working out where a command started by scanning the screen for something that looks like a prompt breaks the first time somebody changes their prompt string, opens a full-screen editor, or prints a line containing a dollar sign, and from then on output is attributed to the wrong command in a store you will later hand to a model. Take the boundaries from the shell: the OSC 133 sequence marks prompt start, prompt end, pre-execution and the exit code, and if the snippet is not installed the terminal says block capture is off instead of guessing.
  2. Program output is hostile input

    Displaying a file somebody sent you cannot make the terminal answer back, set your clipboard or report your window title.

    1. A list you can read beats whatever the parser happened to support.

      Files src/sequences.tsdocs/sequence-policy.md

    2. Assert that nothing travels back up the pty.

      Files tests/hostile-output.test.tstests/fixtures/hostile.bin

    3. The clipboard is somebody else's text arriving at a shell prompt.

      Files src/paste.tstests/paste.test.ts

    done when · tick each as it passes
    watch out
    • Displaying a downloaded file is enough for that file to set your window title, ask the terminal to report it back as if you had typed it, or read your selection out as a base64 string, and xterm ships with allowWindowOps false precisely because of this, its disallowed list naming GetSelection, SetSelection, GetWinTitle, GetIconTitle, GetChecksum and SetXprop. Implement those as no-ops and write a test that pipes a crafted file through the renderer and asserts that not one byte travelled back up the pty.
    • A pasted block of text containing a newline runs as soon as it lands unless paste is bracketed, and a pasted escape character can put the shell into a mode the user did not ask for, which is why xterm converts backspace, delete, enquiry, end-of-transmission, escape, null and the stty specials to spaces on paste by default. Bracket the paste and filter the same class of characters rather than trusting the clipboard.
  3. One door out, with a filter welded to it

    Text cannot reach the network without passing the redactor, and every request that left is recorded.

    1. One door, and the lock is on the door rather than on the corridor.

      Files src/outbound.tstests/outbound.test.ts

    2. Fabricated credentials only, and the suite runs them on every change.

      Files tests/fixtures/credentials/tests/redaction.test.ts

    3. So the question of what left this machine has an answer.

      Files src/outbound.tssrc/db/outbound.ts

    done when · tick each as it passes
    watch out
    • The usual mistake is redacting for display and sending the original, because the highlight and the request get written a week apart by somebody who has forgotten the other exists. Put the filter inside the request function, have it refuse text without a receipt, and prove it by asserting in a test that a block containing a fabricated key never appears anywhere in the recorded request body.
  4. An assistant that proposes and never types

    A suggested command is inert text until a person deliberately runs it.

    1. Key handling and paste. Everything else is a proposal.

      Files src/pty.tstests/no-injection.test.ts

    2. A pipe hidden in a suggestion is the whole attack.

      Files src/ui/Proposal.tsx

    3. The default first run has no key and is a good terminal.

      Files src/config.tsREADME.md

    done when · tick each as it passes
    watch out
    • Pushing a suggested command into the input line so the user can edit it turns a poisoned line in a build log into a command sitting at your prompt, and the model read that log precisely because you asked it to explain the failure. Render proposals as inert rows, show the argument vector rather than a string with pipes in it, and keep a test asserting that the pty write path is reachable only from key handling and paste.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more dev tools.