Prepare a code review locally, with Claude Code drafting comments you edit by hand, then post it to GitHub in one deterministic call.
Fetching a pull request, staging comments, reading the diff on screen, and posting all work.
second-look get 42 # fetch the PR, cache the diff, check it out
# an agent drafts comments through the skill the binary prints
second-look 42 # read the diff, triage the comments, submit with a key
second-look post 42 # or post from the shell in one callA pull request is named three ways: 42 for this checkout's repository,
owner/repo#42, or its URL. The last two need no clone of it. Reading the diff, triaging,
answering a conversation, and posting all come off the API, so a review is prepared and
finished from an empty directory, with its state under your config directory rather than
in a working copy. second-look reviews lists those beside the checkout's own.
second-look inbox, second-look threads, and second-look reviews are three tabs of
one screen rather than three programs. Each command opens on its own tab, 1, 2, and
3 pick one, ] and [ step through them, and each keeps its own cursor and filter, so
going from a pull request waiting on you to the conversation on it costs a keystroke. A
tab loads when it is first looked at, so opening on one pays for that one alone.
The inbox runs its sections at once and draws each as it lands, with the headings saying which searches are still out. Each bucket is ordered for triage rather than by recency: what you have already started here first, then the smallest of what an earlier read rated, then what has waited longest, with drafts under all of it.
A bucket you cannot see all of at once is rated in the background, four diffs at a time, and re-sorts as the answers land while the header counts what is still out. The reader keeps their place: the cursor stays on the row it was on. What was rated is kept under the state directory against the update time it was read at, so a second open orders itself off disk and only a pull request pushed to since is read again. A diff no grammar answers for (a lockfile, a directory of YAML) is recorded as read all the same, and one that could not be fetched at all is not, so a rate limit costs the order rather than the cache. A bucket short enough to read at a glance is left alone, because a read per row buys nothing there.
A row left unrated by either the threshold or the allowance is read once the cursor has stopped on it. Every move restarts the wait, so running the cursor down a queue asks for nothing and stopping on a row pays for that row alone.
How large the change is comes back with the rating, counted off the same patch, and sits in two columns beside it. It orders nothing: a 9 on eight hundred lines and a 9 on nine are different claims, and that is what it is there to say. A hunk that changed nothing but layout counts toward neither side.
Before any of it runs, the queue asks GitHub what is left of the hour's allowance, which is
a read GitHub does not charge for. It spends at most half of what remains: ordering a queue
is what leads to opening the reviews in it, and those are reads too, so a queue sorted
perfectly by an exhausted allowance has spent the budget on the index and none on the book.
Where it stops short the header says so and when more arrives.
The other thing a reviewer would want and cannot have for free is whether they are the
only human asked: gh search prs does not say. / narrows any of the queues to the rows carrying a word, matching the repository, the
author, the title, and the line last said, with the header saying how many are held back.
esc puts them back before it leaves the screen.
C in the review screen is what gets a working copy when you want one: it moves the
checkout onto the pull request, asks before it stashes anything, and draws the screen
again. Cloning stays manual, so C moves a clone that is already on this laptop and says
so when there is none. ! refuses in that case too, rather than opening a shell against
whatever the working directory happens to be.
second-look skill prints the instructions an agent needs to drive it, ready to write
into a skills directory. An installed copy is a snapshot rather than a link, so run it
again after an upgrade.
+ and - grow and shrink the file's own context around the hunk under the cursor. A
hunk carries three lines by convention and the question a review turns on is often in the
fourth, and leaving the screen for an editor loses the comments, the read marks, and the
place. The file is read from the checkout when one holds the commit, which is free and
works offline, and from GitHub when none does.
ctrl+n while writing completes the word under the cursor from what this review already
knows: the files the diff touches, the symbols the structural pass named, and the people
who have said something on the pull request. @ completes a login. Pressing again takes
the next match. Nothing here needs an index, which is why it is what got built; every
symbol in the repository rather than the ones in the diff waits on codeintel.
s on a line of the diff opens the line's own text and stages what comes back as a
GitHub suggestion, which the author commits in one click. Typing three fences and the
line's leading whitespace correctly is why nobody writes one from a terminal. What GitHub
would refuse is refused at staging time: a suggestion has to hang from the right side and
cover only lines the file that results has, so a range crossing a removed line is caught
before it is sent rather than after.
V opens a range on the line under the cursor and closes it wherever the cursor has
reached, so a writes one comment covering all of them and s opens on every line it
will replace. A range beyond one line could only be written by an agent into the TOML
before, because the screen had no way to say where one ends. It is answered by the key
that uses it, so the next comment is never quietly written against the last one's lines.
Several comments on one line read as a run: a heading says how many there are and which
line they answer, each is numbered inside it, and the line itself is pinned to the top of
the frame while the run is read past it. A skip inside a run gathers below the live
comments as one row saying how many, which za opens.
A comment whose line no longer reads the way it did when it was written says so where it is drawn. It is the posting guard asked at read time, because finding out at submit that four comments moved under a force-push is finding out too late.
H compares against an earlier round. A review records every head it has been prepared
against, the diff cached at each one is kept for as long as the review is, and H lists
them so a second pass can hide every hunk the round it names already carried. It reaches
nothing, since both diffs are already on disk, and a hunk is matched by what it says
rather than by where it sits.
U narrows the review to what is new since the last pass: every hunk already marked read
is hidden, and the marks are keyed by what a hunk says rather than by the commit it sat
on, so a hunk that survived a force-push unchanged stays hidden and one that was touched
comes back. It composes with w and W rather than replacing them.
The head is asked about again while the screen is open, not only when it opens, so a push that lands mid-review says so rather than being found out at submit time.
The screen watches the artifact, so an agent writing it while you read reaches the screen
rather than being clobbered by the next keystroke. What is being typed is never
overwritten: where the same comment moved underneath, the buffer keeps the screen and
ctrl+t swaps the version from disk in and out, because resolving that collision without
asking would throw one of the two away.
A comment can be handed back rather than ruled on. m then t marks it todo, which is
the fifth state beside ready, draft, and skipped: it means an agent owes work here, and
posting refuses while any remains for the same reason it refuses a draft. T writes every
todo comment out with its context and runs whatever dispatch in config.toml names,
which is nothing by default, because starting an agent is not something to do on a
keystroke nobody configured. The answer comes back as a turn on the comment, and the
comment comes back as a draft for you to rule on.
Turns are the exchange about one comment rather than one mutating note, and they render
collapsed: the last turn trimmed, one line of the one before it, and a count of everything
older, with za opening the lot. A comment three rounds deep is otherwise a page of prose
between you and the next hunk.
An agent reads the code as well as the review. second-look show <pr> --diff prints the
cached diff with every staged comment marked on the line it anchors to, and
second-look context <pr> <id> prints one comment with its hunk, the note that never
posts, and the conversation it answers. Without them an agent asked about a finding has
only a path and a line number, and what it goes and reads is the working tree rather than
the commit the review was written against.
Every comment is anchored to the diff line it points at. A comment on a line the diff does not carry is refused while staging, and one whose line has moved since is refused before anything is sent.
Conversations already open on the pull request are shown where they anchor, so a second
pass answers what was said last time. e on one writes the reply in $EDITOR.
The queue stages the next few reviews in the background while you read the current one,
in the order the queue is read, so the one after this is ready before this one is
finished. It skips a row this laptop already holds a review for, holds back when GitHub's
hourly allowance is thin, and prefetch in config.toml sets how many to keep ahead (0
turns it off). Leaving a review comes back to the queue rather than ending the session.
second-look inbox is your review queue. enter on a row opens the review, which needs no
clone of that repository, so getting to one costs an API read rather than a clone and a
branch switch. C moves a checkout onto it, m comments on the pull request itself, A
approves it (A again to confirm), and o opens it on GitHub. A pipe or --json gets the
text instead of the screen, in the same triage order, each row carrying what this laptop
already knows about it: whether a review is staged here, and what an earlier read rated
and measured the diff at. So whatever works through a queue reads the cheap rows first
without fetching a diff to find out which those are.
The sections are yours. Without a config it shows three buckets (pending your review, reviewed and still open, then reviewed and merged); with one it shows what you asked for:
# ~/.config/second-look/config.toml
limit = 25
# What this repository writes by machine, added to the built-in patterns rather
# than replacing them. A trailing slash matches a directory anywhere in a path;
# anything else matches the end of one.
generated = ["api/schema/", ".gen.ts"]
[[section]]
name = "needs my review"
query = "review-requested:@me is:open archived:false sort:updated-desc"
[[section]]
name = "my work"
query = "author:@me org:acme is:open archived:false sort:updated-desc"
# What X runs over the review's files beyond the language server. {files} stands
# for the files under review; format is ast-grep, ruff, or text.
[[check]]
name = "house rules"
command = ["ast-grep", "scan", "--json=compact", "-c", "~/.config/sgconfig.yml", "{files}"]
format = "ast-grep"
# A language server for something the built-in list does not cover. An extension
# named here wins the built-in that claims it.
[[server]]
name = "rust-analyzer"
command = ["rust-analyzer"]
extensions = [".rs"]
# Where its project is, most telling first, and what it cannot run without.
roots = ["Cargo.toml"]A query is gh search prs terms, which is what GitHub's search box takes. A sort:
qualifier becomes gh's own flags and a query naming no subject is scoped to what involves
you, so a query written for gh-dash answers the same
way here.
Merging is not on a list row. It is M in the review screen, M again to confirm, and it
refuses while anything is still staged.
Submitting is S and then what the review is: a approves, r requests changes, and c
comments. What the second key says is written back to the artifact, so a review posted as
an approval does not read afterwards as a comment. There is no key for sending it as
whatever the file already says, because that is the decision the confirmation exists to
take. o opens the pull request in a browser, which is where to go the moment it posts.
second-look threads is the queue of conversations across every open pull request you
are involved in, in three buckets: what moved since you last looked, what is still
waiting on you, then what is waiting on somebody else. A conversation is yours when the
pull request is yours, when you have commented in it, or when a comment names you, and it
covers inline review threads, the pull request's own comments, and the bodies submitted
reviews carry.
"New" means new to you. What you have read is kept per conversation under your config
directory rather than in a repository, because the queue spans repositories, so a reply
that arrived while you were away shows up whether or not a notification did. enter
reads a conversation and marks it, R marks one dealt with, and r opens the review
screen to stage an answer, in whichever clone of that repository this laptop has.
A thumbs-up is what R always leaves, because that is the marker a person recognizes and
the only one a pull request comment or a review body can carry. A thread gets the resolve
as well. So anything you have already thumbs-upped is gone from the queue whether or not
GitHub let you resolve it.
A bot reaches that queue only through an inline review thread, which is the one surface where what it says is anchored to code and can be resolved. Its pull request comments are coverage tables and linkbacks nobody ever resolves, and admitting them filled the queue with 77 rows where 13 were real.
A review opened out of that cache was staged against whatever the head was then, so the screen asks the forge whether it still stands and draws nothing but a line saying so until the answer comes back. Reading a diff that turns out to be the older one is worse than waiting one round trip for the question, and a check that cannot reach the forge releases the diff with the failure in the footer.
Every review, read mark, cached diff, and rating lives in one store under the user state
directory, keyed by owner, repository, and number, so a pull request read from two clones
is one review rather than two. An artifact directory left in a working copy is moved there
the first time that review is opened, and a pointer is left saying where it went. A review
the store already holds stays in the working copy instead, since two copies of one pull
request can carry different staged work and overwriting either would lose it; reviews
lists what stayed under "left in a working copy" for a person to settle.
second-look reviews lists what is staged, newest first. enter opens one. Everything it lists is unfinished, because the artifact is deleted the moment a
review posts. d, twice, throws one away along with the diff, threads, rating, and read
marks kept for it.
A pull request based on another one staged here is a stack, and the two are grouped
together with the bottom first, which is the order they read in: a diff against changes
you have not seen yet is a diff you cannot review. The branches are read when the review
is prepared, so a stack is visible where both of its pull requests have a review staged
on this laptop. A pipe or --json carries that order too, which is what lets something
reviewing a batch in turn reach the bottom of a stack first.
Answering a conversation on a repository you are not standing in works too. second-look
asks gh repo-dashboard --cli (from its cache, so no network) which clones of that
repository are on this laptop, uses the one that answers, and offers the choice best first
when several do: already on the branch, then clean, then one that would need a stash. A
repository with no clone here is reviewed from the API instead. It needs
gh-repo-dashboard new enough to print a
remote field, and says so when it is not.
Uncommitted work is the case the move asks about: it names how many files are dirty and
offers to park them with git stash, and git stash pop brings them back. Nothing is
popped for you, and declining leaves the tree as it was. Only a terminal is asked, so a
piped run never has its working tree moved.
P posts one comment on its own for the thing that should not wait, and
second-look post 42 --only <id> does the same from the shell.
w hides hunks that change nothing but whitespace, and says how many it hid. W
hides every hunk a parser says changed no code, so a re-wrap across lines and a
reworded comment go too. That needs ast-grep on the
path, because every tree-sitter binding for Go needs cgo and the release builds
ten platforms without it; W says so when it is missing and w never needs it.
The same pass rates the change, which is the cost in the title bar. A signature
change outweighs anything a body does, because every caller of the symbol is in
scope whether or not the diff shows one; a capability the change reaches and the
base did not counts next; and size is only the tiebreaker. The number is
advisory, deterministic, and decides nothing.
Files are grouped by directory with file and hunk counts on each heading, and ]d walks
the groups. One directory over a review whose files each carry their whole path is the
same words twice, so that heading appears once there is a second group to tell it from.
The title names the file the cursor is in once that file's own heading has scrolled off
the top, beside where the frame sits and which comment the cursor is on. A heading you can
still see is not worth the width.
The rest of the frame is what somebody has to act on. A count of zero says nothing
happened and is left out, and a working copy standing where the review was staged says
nothing either, so the title carries off head or no clone only where a key would
refuse to work and colors that one fact alone. Each file heading says how many lines it
adds and removes, which is what the header would otherwise be asked for. The review's own
body and note start folded, because you wrote them and open they cost nine rows before the
diff began. A hunk keeps a row of its own, since that is what ]h and ]u walk and what
carries the read mark, and what the row says is the declaration git named as enclosing it
rather than @@ -0,0 +1,57 @@, which is the gutter beside every line said twice. Where
git named nothing, the row is a rule, and a file drawing one such hunk is that hunk, so
its heading carries the read mark and the hunk's row is left out.
c walks three views: both, the code, the comments. Both is the diff with what is being
said about it inline. The code is the file as it reads after the change, where a removal
stands as one line saying how much came out and a comment stands as one row, both of which
za opens where they are, because a +/- pair leaves working out what the code now says to
the reader and four comments on one hunk bury it. The comments are what will post, by file. The cursor keeps
its comment across the change.
The head is asked again once a minute, because a review read over twenty minutes outlives
the answer given when it started. When it has moved, ctrl+r prepares the review again
against it in place, keeping every hunk already marked read and naming the staged comments
that no longer anchor. It moves no working copy: where the tree stands is C's question.
t is the fourth view and is off that cycle: every open conversation on the pull request,
each under the line it answers, with the rest of the diff left out. A conversation is what
the forge already holds rather than what this pass is staging, which is a different axis
from how the change itself is drawn, so a second t goes back to the diff however the view
was reached. GitHub's own list interleaves the diff, which spreads what is still being
asked through a page of code nobody is rereading.
Hunks are gathered by symbol rather than left in the diff's order. A group is a symbol some hunk declares together with every hunk that calls it, wherever in the diff they came from, so the callee whose signature moved sits next to the caller that has to change with it. Everything no symbol gathered keeps the directory grouping. Groups are ordered by what their hunks cost to read, so a signature change comes before a renamed local.
A name is matched rather than resolved, so a name more than one hunk declares gathers
nothing: New and Error are one symbol to a matcher and gathering on them would put
half a diff under a heading that lies. O puts the diff's own order back, which is the
way out when the gathering guesses wrong and the way to consult what the forge thought the
order should be. A file whose hunks landed in two groups says so on its heading and ]f
walks to the rest.
Files a machine wrote are grouped last, folded, and counted rather than read. Lockfiles,
go.sum, *.pb.go, snapshots, and vendored trees are recognized without configuration;
generated in the config names whatever else this repository writes. What matters about
one is that it moved and by how much, so the group says how many files and hunks it holds
and za opens any of them. A lockfile in a format this reads (go.sum, go.mod,
Cargo.lock, uv.lock, package-lock.json, pnpm-lock.yaml, yarn.lock,
Gemfile.lock) draws what moved rather than a hunk count, and L on it asks
osv.dev what is known against the versions it moved to. That is the
only thing here that reaches anywhere but GitHub, so L puts the question up and names
what it will send, and a second L sends it. The answer is drawn under the dependency it
is about, leading with the version it was fixed in, and the file's own row says when the
question was refused: being offline, being rate limited, and a private registry all read
the same way, and a card that quietly omitted a package would be worse than no card at
all. OSV also says the same thing about a package it has never heard of as about one it
has and finds clean, so "nothing known against it" is not the same as "every name
resolved".
A comment from GitHub is markdown somebody wrote for a browser, and a bot's is mostly
machinery: routing state in an HTML comment, the scripts it ran, and the output of each,
inside collapsed sections that a terminal has no way to collapse. So a comment is
segmented before it is drawn. The HTML comments are dropped, since they are addressed to
the bot and not to you. A <details> section is one row you open with za, the way GitHub
draws it. A fenced block longer than ten lines is drawn to its first six with the rest a
keystroke away, and is never wrapped, because reflowing code says something the code does
not; where the fence names a language, it is colored by that grammar. Tables and quoted
lines are cut at the frame rather than reflowed for the same reason. Inline emphasis
markers are taken out, since a terminal row has one face and _🩺 Stability_ spelled out
is worse than the words alone; backticks stay, because they mark code and nothing else in
a terminal does. Any comment still over twenty rows after all that ends in a count of what
is left, which za opens.
X is what a checker makes of the change. A review is read against a diff, and the
questions it turns on are often ones no diff can answer: whether the field being read off
a value is a member of the type it actually has, whether a rule nobody's CI enforces would
object. Both are answered by a program already on the machine, so second-look starts it
and reads its answers back.
The language server is the one your editor uses, started in the project the file belongs
to and spoken to over its stdio. In a monorepo that is the directory above the file
holding a tsconfig.json, a go.mod, a pyproject.toml, or whatever else marks a
project for that server, because a server rooted at the whole checkout compiles the file
under settings that are not the project's and reports errors the change did not cause. The
markers are ranked before the directories are walked, so a go.work three directories up
wins the go.mod beside the file: they mark different things and the workspace is the one
that resolves the siblings the file imports. A directory a server cannot run in is walked
past rather than started in, which is what a package carrying no typescript of its own
is. A server asking what it is configured with is answered from settings in its
[[server]] entry. Each project answers at the same time as the others, so a change
touching four packages costs what one package does. typescript-language-server, gopls,
and pyright-langserver are recognized without configuration, and [[server]] in the
config names any other. The file it is asked about travels as an unsaved buffer holding
the commit under review, so nothing writes to the working tree and a checkout left on
another branch still answers for the file. What that file imports is resolved from the
tree as it stands, which is the limit this ships with. The first file costs a few seconds
against a monorepo, because a server loads the project before it says anything, and every
file after it a fraction of one. It runs behind the first frame for that reason: the diff
is on screen while the pass is out, and the count reaches the title when it lands. A
message longer than three lines is cut where it is drawn, since a type error names the
whole of an anonymous type and the first line of it already said what is wrong.
[[check]] names whatever else is worth running over the same files, which is where a
rule your repository's CI does not enforce goes: a house rule about comment length written
as an ast-grep rule, a ruff selector the project has not adopted. A check is a command and
the shape it prints in, ast-grep, ruff, or text for path:line:col: message, with
{files} standing for the review's own files. A check naming roots is run once per
project rather than once per checkout, and bin names the directories under a project
holding the tool that project pins, which is searched before your PATH: a linter run
from the PATH is a different version under different settings than the one the project
installed, and the two disagree about the same file.
A note is drawn under the line it is about, because the message is the whole of it: a mark
in the margin says a line is wrong and leaves you to go somewhere else to find out how,
which is the trip out to an editor this exists to save. A note on a line the change only
carried is context rather than a finding, so it waits in the trouble list instead of
sitting in the diff. ]p walks the notes, X lists every one of them under the line it
lands on, and K asks what each name on the line under the cursor actually is. All of it
needs a checkout, since a server resolves what a file imports from the tree around it and
a review staged with none has no tree.
v walks the renderers, which change how a line of the diff is drawn rather than which
lines are drawn. rich is what a review opens on: it colors the code by its grammar,
says which side a line is on with a band behind it, marks the runs that actually differ
from the line the change paired it with, and carries both line numbers in the gutter, so
a one-word edit reads as one word. plain is a whole-line color per side and one number
per line, which is what a terminal diff has always looked like, and is where v lands
last.
Four things carry a change in rich, because no one of them survives every terminal. The
+ or - sits against the code on a cell of full accent, which is the loudest thing on
the row and costs no width. A rule closes the gutter, so the code has a left edge to run
down. The runs that actually differ are underlined as well as deepened, because a
background can only get so far from the band under it before the code sitting on it stops
being readable. And a context line is drawn at half saturation, so a changed line is the
vivid one rather than merely the banded one. The separations are measured in CIEDE2000 and
WCAG contrast by a test, on both palettes and at both color depths, so a palette change
that flattens the diff fails a build rather than a review.
split is the same faces side by side, each removal beside the addition that replaced it,
so an edit reads as one row rather than as two. Each half numbers the file it is showing,
which for a context line means two different numbers the moment anything above it was
added or removed. Below 120 columns it draws unified instead of wrapping two columns of
code into illegibility. It applies to the diff and not to the code or comment views, which
are flattenings of their own.
structural is the rich faces with what the parser saw written onto every heading: which
symbols a hunk touched and how, the same summarized under each file name, and a symbol
that left one hunk and arrived in another drawn as a move rather than as a delete beside
an unrelated insert, whether or not the two hunks are in the same file. It reuses the pass
the review-cost rating already runs, so it costs no extra work. A symbol whose declaration
was rewritten on the way is not a move, because it is not the same code arriving somewhere
else.
The grammar, the columns, and what the parser saw are three independent questions, and v
only walks the four combinations worth naming. u toggles one on its own: ug the
grammar, us side by side, up what the parser saw. So a wide diff can be read in two
columns with the grammar off, which no step of the cycle offers. A look reached that way
belongs to no step, so the next v starts the walk again from the top.
All three are spikes and ship with their caveats named in the footer when you switch to
them: a
hunk is a fragment of a file, so a grammar's state above the hunk is lost, a hunk opening
inside a block comment is colored as code, and the symbol a body edit sits inside is not
knowable at all. Highlighting is
chroma, which is pure Go and keeps the release
matrix building with CGO_ENABLED=0, unlike every tree-sitter binding. The bands are
drawn deeper on a terminal that cannot mix its own colors, because the 256-color cube
carries almost no dark tints and both sides otherwise quantize into the grey ramp.
a then b, m, n, t, or ? writes a comment on the line under the cursor, ranked
blocker to question (q is every chord's cancel, so a question is ?). The editor opens under the line, and ctrl+s stages the comment
ready with the line it anchors to already quoted, so nothing has to be restamped and the
post guard has what it needs.
e opens an editor in the frame, in place of the block it is writing, so the line being
answered stays on screen. ctrl+s saves, esc leaves it, and ctrl+e hands what is typed
to $EDITOR for the edits a text box is the wrong shape for. It writes a comment, an
answer to an open thread, and the review's own body and note. Saving an edit marks a draft
ready, since writing the comment out is the ruling a draft was waiting for; a skip keeps
its status, because a skip is a decision with a reason against it.
An edit left unfinished is kept beside the review rather than thrown away, so a terminal
that dies mid-sentence loses nothing. The next e on the same thing opens on what was
left, says how long ago, and ctrl+r puts back what the field says now, which is the
other half of the decision.
S then a, r, or c submits, approving, requesting changes, or commenting. The second
key both confirms the post and says what kind of review it is: there is no key for "send it
as whatever the file already says", because that is the decision the confirmation exists to
take.
m then r, d, or x marks a comment ready, draft, or skipped. It is a chord because
each of those restamps whatever the cursor is on, irreversibly, and three unmodified
letters next to the motion keys made that one keystroke away.
z folds what the cursor is on: a whole file from its name, one hunk from anywhere inside
it, a comment's note from the comment, and in the code view the run of removed lines from
the row standing for it. za inverts it, zi inverts the whole review, zR opens
everything, and zM folds to the file names, which is the outline a long review is read
from. A comment's note is drawn until you fold it, because it is the evidence for the
comment above it and one folded by default is one nobody reads. What a machine wrote, and
the review's own body and note, are the exceptions: the first is worth knowing moved
rather than reading, and the second you wrote yourself. A folded heading recedes to the
contrast of a hunk already read, so what is closed reads as closed.
/ searches, and tab in the prompt restricts it to hunks you have not read yet. The
pattern becomes a motion, so n walks the matches the same way it walks hunks.
ctrl+e and ctrl+y scroll without moving the cursor, so a glance at what is above or
below costs nothing to come back from: the next motion pulls the frame back to where you
were.
space marks a hunk read and ]u goes to the next unread one, so a long review is
finished when nothing answers ]u. What is read is keyed by the hunk's content, so a
force-push that leaves a hunk alone leaves it read.
! drops to your shell in the repository and attaches what the session printed to the
comment under the cursor, so a comment carries the output that proves it. That note is
local and never posted. It refuses while the checkout is on another branch or missing,
since a shell there would run against something other than the diff.
- Next steps — what is open, in the order it wants doing
- Requirements — scope, decisions made, and what is still open
- Prior art, August 2026 — what already exists in this space and the three capabilities nothing implements
Homebrew, once a release exists:
brew install --cask kyleking/tap/second-lookOr from source:
go install github.com/kyleking/second-look/cmd/second-look@latestThe binary is second-look. Every command below is short enough to type through an
alias, which is what I use:
alias sl=second-lookmise install && hk install --mise
mise run ciSee CONTRIBUTING.md for the full workflow.
