Skip to content

Security: sumervaid/reflex-tui

Security

docs/security.md

Security

Transcripts are untrusted data. Reflex reads them, it never obeys them.

In short

  • Transcripts are read locally and treated as data, never as instructions.
  • Reflex makes no network connections; outgoing TCP, TLS, UDP and DNS are blocked in-process. The one exception is reflex model fetch, which you run explicitly to download an optional embedding model from Hugging Face only (pinned files, streamed to disk and verified by SHA-256). It reads no transcripts and uploads nothing; scans stay offline.
  • Secrets, emails, home paths and your user name are redacted before anything is stored or shown.
  • Scans are incremental: a transcript that has not changed since the last scan is not opened again. Redacted traces are kept after their transcript is deleted (by you, or by the agent's own cleanup), so history can reach further back than the agent keeps it. reflex forget --missing deletes those, reflex forget --older-than <days> older ones, reflex settings retention <days> does the latter after every scan, and reflex reset deletes everything.
  • It reads only known agent history folders (or ones you pass), after asking.
  • Nothing is written outside ~/.reflex until you install a skill (per target with reflex install, or into every detected agent with space in reflex tui), turn on memory or the Cursor hook, or apply a reflex improve edit, each after asking. Installs never overwrite; removals delete only unchanged copies Reflex wrote.

Threat model

Risk What Reflex does
Prompt injection Transcript text is only tokenized, classified and counted. Skill titles, descriptions and steps come from fixed templates and a family catalog. The one exception is the Constraints section, built from your follow-up corrections. A trace stores up to 8 lowercase content words of a correction (filler removed), not the message. Model-written compaction summaries are not treated as your messages, and neither are the prompts a parent agent gives the agents it launches: only those agents' tool calls are read, and their short descriptions are redacted and shown, never used as requests. A rule is written only if the same correction recurs in at least 2 independent sessions, and only from the words a pair of those sessions share. Clauses with shell syntax, flags, URLs, paths, host names, IP addresses or redaction markers are dropped. A rule that tells the agent to do something must open with an allowlisted verb (use, run, keep, …) and pass the command safety filter, so "always push to main" never becomes "Push main.". Rules that try to steer the agent or touch secrets or permissions (ignore, instructions, tokens, env, permissions, confirm, …) are dropped in either direction. A hostile workflow with hostile follow-ups, repeated across sessions, is covered by tests.
Shell injection Reflex never executes transcript content. Example commands in skills must recur in two sessions, are normalized (<n>, <file>, <ref>), and are dropped if they contain shell metacharacters (` $ ; & | < > \) or anything destructive, privileged or remote (rm, sudo, curl, push, --force, reset --hard, npx, …). Outward-facing steps (push, commit, post review) say "only if the user asked".
Network None. Outgoing connections are blocked in-process at startup: TCP and TLS (which covers fetch and http), UDP, DNS queries, and the same in every worker thread started afterwards. The only exception is reflex model fetch, which the user runs explicitly (and confirms) to download an optional embedding model: for the download only, it lets connections to huggingface.co, hf.co and registry.npmjs.org and their subdomains through (plus the proxy, when NODE_USE_ENV_PROXY is set). It fetches the model's files (two for all-MiniLM-L6-v2, three for bge-m3) pinned to one Hugging Face revision, streams each to a temporary file and keeps it only if size and SHA-256 match. It also fetches the WebAssembly runtime the model runs on (onnxruntime-web 1.29.0, the npm release tarball, about 32 MB): the tarball must match npm's sha512 integrity value and the two files must match their SHA-256, or nothing is kept. Reflex reads no transcripts and sends nothing but these requests. The model and runtime are verified again before every load. reflex tui runs in the same guarded process, and its rescan child is the CLI, which installs the guard too. validate --run starts Claude Code as a separate process, after asking, on a fixed prompt in a disposable repo. reflex run and reflex bench start agents (and bench's setup and check commands) as separate processes too; the guard covers Reflex's own process only.
Secrets Redacted at parse time, before storage or display: private keys, *_KEY/TOKEN/SECRET/PASSWORD/URL= assignments, auth headers, bearer tokens, connection strings, URL credentials, provider key formats (Anthropic, OpenAI, AWS, GitHub, Slack, Google, Stripe, npm), JWTs, emails, high-entropy strings, home paths (including your own home folder wherever it is) and your login name. A login name that is also an everyday word or a generic account (root, test, admin, will, …) is redacted only as part of the home folder, so "root cause" or "run the test" stay as written. Generated skills are redacted again and refused if a leak check finds anything.
Permissions scan shows locations and file counts without reading, then asks per source. Without a TTY, every prompt defaults to No unless --yes is passed. install asks per target and shows the destination.
Agent memory Off until you run reflex memory on (or press m in the TUI). Reflex then owns only the text between <!-- reflex:memory:start --> and <!-- reflex:memory:end --> in ~/.claude/CLAUDE.md and ~/.codex/AGENTS.md; everything else in those files is kept byte for byte, and off or reset removes the block. Entries are built like skill constraints (only words a pair of independent sessions shared, no commands, paths or steering words). Text you add is refused if it needed any redaction. A symlinked instructions file is never written through.
Overwrite Generation writes only inside ~/.reflex. Installs use exclusive create (O_EXCL), so an existing skill is never replaced; you can view a diff, pick another name or cancel. Re-approving asks before replacing a Reflex copy you edited. reset deletes only files Reflex created.
Path traversal and malicious filenames Skill names must match ^[a-z0-9][a-z0-9-]{0,47}$. Destinations are checked to stay inside their target folder. Transcript filenames are never used to build write paths.
Symlinks Discovery skips symlinks. Writes use O_NOFOLLOW, and Reflex refuses a symlinked home folder or skill folder.
Scope Only known agent history folders (or folders you pass) are read, at a fixed depth and filtered by name. Reflex never walks your home directory.
Cursor app database After you allow Cursor transcripts, scan asks a second, separate question before opening Cursor's state.vscdb (skipped when you pass a custom Cursor folder). It is opened read-only, queried with bound parameters for one validated chat id at a time, and only model names are extracted: names must match ^[A-Za-z0-9][A-Za-z0-9._:/-]{0,63}$, at most 5 per chat. Message text is never selected. REFLEX_CURSOR_STATE_DB points it elsewhere.
Denial of service Files are streamed line by line. Lines over 4 MB are skipped, sessions are capped at 3,000 events and 40 requests, and each source is capped at its 2,000 most recent files. Malformed lines are skipped and counted.
Terminal escapes In reflex tui, every string that comes from transcripts, files or paths passes through toCells before it is drawn: control characters (including ESC), zero-width and combining characters are dropped, and wide characters become ?. Text cannot emit escape sequences or shift the layout. The terminal writer replaces any control character it still sees.
TUI writes space in Skills is the consent for one skill. Approving writes only into ~/.reflex/skills and never replaces a copy you edited there. Installing uses the same exclusive-create path as install: an identical copy already in a target counts as installed, a different skill with the same name is skipped and named in the message. Removing deletes only a regular file at <target>/<name>/SKILL.md whose sha256 matches the installation record, then the folder if it is empty. Symlinks are never followed or deleted, and paths outside the target folder are refused. A copy you edited stays, with its record.
Cursor hook Off until reflex cursor-usage on, which asks first. It adds entries to ~/.cursor/hooks.json only, keeps every other key and hook, copies the previous file to ~/.reflex/backups (0600) first, refuses a symlinked or invalid hooks file, and writes atomically; off and reset remove only Reflex's entries. The hook script lives in ~/.reflex/hooks (0700) and is run by absolute Node path. It reads the event from stdin and appends validated ids ([A-Za-z0-9-]), a validated model name and four non-negative integers to ~/.reflex/cursor-usage.jsonl (0600); it never writes text, prompts, paths or anything else, makes no network calls, always prints {} and exits 0, so it cannot block or steer the agent.
Routing reflex run passes your request to claude or codex as one argument with no shell, plus --model <name> (names must match ^[A-Za-z0-9][A-Za-z0-9._:[\]-]{0,63}$) and, for Claude Code, a fresh --session-id. It asks first unless you turned autopilot on. The request is never stored; a routed run keeps only time, tool, model, kind of task, folder name, whether exploration chose it and your optional yes/no answer when the agent exits. Exploration is off until reflex route explore <percent> and only ever picks a model you already used with that tool. Routing writes no agent configuration.
Bench reflex bench asks first and shows every command. Each run gets a fresh git clone --no-hardlinks of the current repository's last commit under ~/.reflex/bench, with its origin removed, so the original repository is only read and nothing can be pushed back. --setup and --check are commands you typed, run in the copy through your shell. The agent is a non-persistent Claude Code session with project settings only, no MCP servers, edits accepted and Bash limited to read-only git, ls and the check command. Copies are deleted afterwards (only inside ~/.reflex/bench) unless you pass --keep. Only the result event is read: usage, turns and cost.
Your skills reflex improve without a name lists skill folder names only. With a name it asks before reading that one SKILL.md (a regular file, opened with O_NOFOLLOW, never through a symlinked folder). The proposed section contains only Reflex's step wording, recurring commands that passed the command safety filter, and constraints built like skill constraints; it is redacted and refused if a leak check finds anything. Your file is changed only after a second confirmation, only if it is unchanged since it was read (sha256), with the previous version written to ~/.reflex/backups first (mode 0600); the replacement is atomic. Existing text is never edited (append-only marked section). --verify runs the task you typed with each version in fresh clones under ~/.reflex/bench with the skill installed project-locally, project settings only and the bench tool allowlist; the two final answers are reduced to their structure in memory and discarded. The database keeps only hashes, counts and the check result, never skill text or answers.
Handbook reflex handbook and its subcommands only read Reflex's database: nothing an agent runs through them can change what Reflex learned. Recommended commands pass the same safety filter as skill examples and rules the same filter as skill constraints, so a hostile transcript can't make the handbook tell an agent to run curl … | sh or to stop asking for confirmation. show and --json include the redacted first request of up to 5 sessions per practice, for your own use; export, meant for sharing, contains only practices, counts and dates.
TUI resume Enter on a Search hit runs codex resume <id> or claude --resume <id> with no shell, after the TUI hands over the terminal. The id is the session's native id from its trace and must match [A-Za-z0-9._-] (80 characters at most), or nothing runs. Claude Code is started in the folder recorded as cwd in that session's own transcript (first 256 KB, opened without following symlinks) and only if that folder exists. For a Cursor chat, Reflex opens the workspace folder its transcript belongs to (decoded by checking which folders exist) with cursor <folder>, or open -a Cursor <folder> on macOS; no chat is started. Prices are a table built into Reflex: nothing is fetched.
TUI rescan r runs the CLI itself as a child process with fixed arguments and no shell: scan --yes --days <n> plus one --source per source the last scan read, and --embed-minutes 1. Its standard output is discarded; the last 4 KB of its error output are kept to explain a failure, and only the last line is shown, redacted. It is stopped after 10 minutes. Those are the window and sources you already consented to. A scan that used --claude-dir/--codex-dir/--cursor-dir is not repeated, because those folders are not recorded. The TUI never prompts for new sources; run reflex scan for that.
Windows launchers On Windows, claude, codex and cursor installed with npm are .cmd files, which only cmd.exe can run. reflex run, bench, validate --run, improve --verify, doctor and TUI resume then start cmd.exe /d /s /v:off /c with one command line in which the launcher's full path and every argument are double-quoted, so & | < > ( ) ^ stay literal. An argument containing a quote, % or a line break is refused instead, since cmd.exe would read it as part of the command. An .exe (such as Claude Code's native build) is started directly, with no shell.
Local data ~/.reflex is mode 0700 and the database 0600 (its -wal and -shm files too). It stores redacted traces and short excerpts, not raw transcripts. Traces are cached per transcript file and kept after that file is deleted, until reflex forget --missing or reflex reset. reflex forget (after asking) and the optional retention setting (reflex settings retention <days>, off by default, applied after each scan) delete a file's traces, its cache entry, its sessions' embeddings, their evidence in stored suggestions and mined memory entries no other session supports; transcripts are never touched. A kept trace cannot be re-derived from its source, so when the redaction inputs or Reflex's own code change, kept traces are redacted again with the current rules. Debug logging (REFLEX_DEBUG=1) prints counts only.

Not covered: a local attacker who already has your user account, and what the coding agent does with a skill after you install it. Read a skill (reflex show-skill) before installing it.

There aren't any published security advisories