σκέψις: viewing, perception by the senses. Examination, speculation, consideration.
A fast, fully local browser-based code review UI. Review your (or your agent's) diff before inflicting it on the world.
- Fast even with huge diffs
- GitHub-style split or unified diff view with syntax highlighting
- Mark files as viewed
- Inline review comments are written directly into source files as code comments or piped to a specified command
- Works with jj and git (tries jj first, falls back to git)
- Vim-style navigation shortcuts (press
?to see the list)
2026-04-02-skepsis-demo.mp4
Requires Node.js v22+.
Run it without installing:
npx @oxide/skepsis
npx may reuse a cached package version. To force the newest published
version, run:
npx @oxide/skepsis@latest
To install a persistent command:
npm install --global @oxide/skepsis
skepsis
Either way, you may want a shorter alias:
alias sk="npx @oxide/skepsis"
Takes -r/--revision, -f/--from, and -t/--to flags for specifying
the commit range. The VCS is auto-detected. In jj mode, the flags are passed
straight to jj diff. In git mode, they have to be translated slightly. See the
examples below.
Each invocation picks a free port by default, so you can run multiple instances
simultaneously. To use a specific port, pass --port/-p:
skepsis --port 8080
Use --host as well if the server needs to be reachable from another machine:
skepsis --host 0.0.0.0 --port 8080
In --dev mode, --port sets the browser-facing Vite port; the proxied API
server continues to use a free port.
skepsis opens the page in a browser when it starts on localhost. Pass
--no-open to skip that, for example when a script starts it in the
background: once the server is listening, it prints one line to stdout,
skepsis on <url> (<diff command>).
The URL ends in #token=…, a secret made fresh on each run. The page needs it
to talk to the server, so open that URL, not just the host and port. After that,
reloads and new tabs on the same address work in that browser; a different
browser needs the full URL again.
To open it some other way, such as in a particular browser or a window of
its own, pass --open-cmd <command> (or set SKEPSIS_OPEN in the
environment). skepsis runs it with sh -c, with the URL appended as the last
argument:
skepsis --open-cmd 'open -a Firefox'
--dev mode ignores it, since Vite opens the page itself.
The examples use skepsis, the installed bin name. An sk alias works the
same way.
skepsis # review branch vs trunk, GitHub PR style
# (jj diff --from 'fork_point(trunk() | @)')
skepsis -r @ # review working copy only
skepsis -r 'mybranch..@' # review a range
skepsis -f main -t @ # diff between two revisions
Ranges passed with -r are passed through verbatim to git diff.
skepsis # review branch + working tree vs origin
# (git diff --merge-base origin/HEAD; falls
# back to origin/main, origin/master, main,
# master)
skepsis -f main # diff since main: git diff main HEAD
skepsis -r main..my-branch # review commits on my-branch
skepsis -r HEAD~5..HEAD # review the last 5 commits
skepsis -f v1.2.0 -t v1.3.0 # compare two tags
skepsis --git # force git in a jj-colocated repo
The CLI starts a local HTTP server that shells out to jj diff
or git diff, then serves the diff to a React frontend that uses
@pierre/diffs to render it with syntax highlighting.
While the page is visible, it reruns the diff every few seconds in the
background, and again as soon as you come back to the tab. When the diff has
changed, it shows a toast with a Refresh button and a list of which files
changed and by how many lines. The displayed diff never changes under you on
its own. Pass --no-poll to turn this off.
The npm package ships compiled CLI/server JavaScript plus a prebuilt frontend
bundle, so installing or running with npx does not build anything. In dev
mode (--dev), a Vite dev server runs alongside the API server with hot
reload from a source checkout.
Review comments are inserted into the source files, so they show up in your VCS
diff and are visible to coding agents. They can be resolved (deleted) from the
UI. Comments are only enabled when the diff includes the working copy (e.g.,
a revset ending in @ for jj, or an open-ended range ending at the working
tree for git), since that's where the inserted lines land. The default diff
in both modes ends at the working copy, so comments are enabled by default.
A comment goes in right after the line it's on. A comment on several lines goes after the last of them, and its open tag says how many it covers, counting up from the tag:
const start = Number(match[1])
const end = Number(match[2])
// <review covers="2 lines above">
// What about NaN?
// </review>Viewed state is stored per file in ~/.local/share/skepsis/ using the file's
content hash (the git object ID from the diff header). If the file changes, the
hash no longer matches and it automatically shows as unviewed again.
--send <command> (or SKEPSIS_SEND in the environment) adds a Send button
to the comment box, which pipes the comment to a shell command instead
of writing it into the file. Use it to pass review comments straight to a coding
agent, a tmux pane, or a log. There is not chat UI in skepsis (yet) so replies
from the agent will appear in that session, wherever it is. Send works
in some cases that inline comments can't (because there is no file to write): on
removed lines, and on diffs that don't end at the working copy. The Comment
button continues to write a <review> marker in the file as usual.
The only way to set the command is --send or SKEPSIS_SEND. skepsis never
reads it from the repo contents, so reviewing someone else's branch
can't make it run a command they chose. Only the page skepsis opened can
trigger a send (see Security below).
skepsis runs the command with sh -c in the directory it was started in. The
command inherits skepsis's environment (which is how the agent samples find
their session), plus SKEPSIS_URL, the page's address, and SKEPSIS_ROOT, the
directory skepsis was started in. It gets the comment on stdin as one line of
JSON whose prompt field is the whole comment as a message. Some sample
commands to start from:
# Claude Code. CLAUDE_CODE_MESSAGING_SOCKET is only set in commands the session
# runs; from any other shell, use the session's socket path instead
# (/tmp/cc-socks/<pid of claude>.sock)
SKEPSIS_SEND='jq -c "{type:\"user\",message:{role:\"user\",content:.prompt}}" | nc -U "$CLAUDE_CODE_MESSAGING_SOCKET"'
# Codex. CODEX_THREAD_ID is likewise only set inside the session
SKEPSIS_SEND='codex queue --thread "$CODEX_THREAD_ID" --message "$(jq -r .prompt)"'
# a tmux pane named review, pasted so newlines don't submit early
SKEPSIS_SEND='jq -r .prompt | tmux load-buffer -b skepsis - && tmux paste-buffer -p -d -b skepsis -t review && tmux send-keys -t review Enter'
# a log file
SKEPSIS_SEND='jq -c . >> ~/review-comments.jsonl'JSON format and exit status
The JSON sent to stdin looks like this:
{
"version": 1,
"root": "/home/me/project",
"diff": { "command": "jj diff -r @-", "toWorkingCopy": false },
"file": "src/a.ts",
"side": "additions",
"lines": [42, 45],
"code": [{ "lines": [41, 46], "text": "const z = 0\nconst a = 1\n…" }],
"blob": "9daeafb",
"text": "why two?",
"prompt": "Review comment on src/a.ts, lines 42–45 (new side of `jj diff -r @-`), in lines 41–46:\n\n```\nconst z = 0\nconst a = 1\n…\n```\n\nwhy two?"
}side is additions or deletions, and lines (1-based, inclusive) are in
that side's numbering. code is an excerpt of that side around the lines
commented on, in runs of consecutive lines, each with its own first and last
line. There's more than one run when the lines commented on span lines the
diff doesn't show. blob is the ID of the
version of the file those line numbers refer to (old or new, by side), or
null where the diff has none, as for an added file's old side or a pure
rename. It's the version on the page, which is older than the file on disk if
the file changed since the page last refreshed. prompt is all of it as plain text, for consumers that only want a
message.
Exit 0 means the comment was delivered, and the page keeps the comment in the diff, under the line it was on, until you dismiss it or reload. skepsis ignores stdout. Any other exit, or running past 10 seconds, is a failure: the page shows stderr (or the exit status) and keeps the comment in the box. Sends run one at a time, in the order they were submitted.
Notes on security
A request to /api/send can't set the command, but it does pipe arbitrary text
to it, so lock down who is allowed to hit the endpoint by requiring a token. The
token is generated per run of the server and passed to the page in the #token=
fragment of the URL it opens. The page keeps it in localStorage, which only
pages on that host and port can read. This keeps out other websites in the
browser and anything coming in through a forwarded port. It doesn't keep out
other users on the machine or other processes running as you.
As a backup, --send also requires a loopback --host, and /api/send rejects
requests whose Host isn't a loopback name or whose Origin
isn't the page's own.
Clone the repo when working on skepsis itself:
git clone https://github.com/oxidecomputer/skepsis.git
cd skepsis && npm install
Running from a checkout requires Node v22.18+, since the CLI runs as TypeScript directly via Node's built-in type stripping. Point an alias at the clone to run it from any repo:
alias sk="node ~/repos/skepsis/cli.ts"
--dev runs a Vite dev server with hot reload alongside the API server:
sk --dev
Before committing changes, run:
npm run ci
- patch — bug fixes, dependency bumps, internal changes
- minor — new features: flags, keyboard shortcuts, UI capabilities
- major — breaking changes to anything scriptable or on disk: removed or
renamed flags, changed flag defaults, changed
<review>comment or viewed-state formats, removed or changed fields in the--sendJSON (adding a field is minor)
To publish a release, bump the version and land the change on main:
npm version patch --no-git-tag-version # or minor/major — bumps package.json
jj commit -m 'v0.1.1' # land the bump on main via PR
On every push to main, CI checks whether the package.json version is
already on the registry. If not, the Release workflow pauses in a "waiting for
review" state. A required reviewer must approve the release environment
deployment from the workflow run before the publish proceeds. After
publishing, it creates a matching GitHub
release like v0.1.1 with
auto-generated notes listing the PRs merged since the previous release.
To publish a canary build of a PR, go to the Release
workflow,
click "Run workflow", and enter the PR number. It publishes the PR's head as
<version>-canary.<pr>.<sha> under the canary dist-tag and comments the
version on the PR. The dispatched workflow requires the same release
environment approval before publishing, and only accepts PRs from branches in
this repo, not forks.