keeper opens a folder of photographs and film and helps you pick the good ones and cut them to the shapes a website or an instagram post needs. it runs on your own computer. it does not need an account and it does not need the internet.
a shoot comes back as 1,768 frames and a website has sixteen holes. every hour between those two numbers goes on scrolling a finder window at 200px a thumbnail, and that is the hour this takes back.
it was written against a photographer's archive and it is not only for photographers. a pile of frames and a handful of holes to fill is the same job whether the pile came off a camera, a phone, a drone or a screen recorder.
keeper can also sort a folder into groups you name, by sending small contact sheets to a model, and that part is off until you turn it on. if you never turn it on, nothing here has changed: no request is made, nothing of yours leaves the folder, and it still runs with the wifi off.
every frame in these shots is a generated stand in. real work does not go in a readme.
everything is on the releases page and there is nothing to install alongside it.
on windows, run the .exe setup and open keeper from the start menu.
on a mac, open terminal and paste these two lines. terminal is in applications, utilities, or hold cmd, press space, and type its name.
curl -L -o ~/Downloads/keeper.tar.gz https://github.com/gntrs/keeper/releases/latest/download/keeper-macos-arm64.tar.gz
tar -xzf ~/Downloads/keeper.tar.gz -C /Applications
keeper is then in your applications folder and opens like anything else. the
first line fetches it and the second unpacks it, and you can throw the
download away afterwards. that is for an apple silicon mac, which is every mac
sold since late 2020. on an older intel mac, arm64 becomes x64 in the
first line.
there is a .dmg on that page as well and it holds exactly the same app. it
is the familiar way to install something on a mac and it is the one your mac
will argue with, which is why it is second here.
your computer will warn you, and here is why. letting a program pass without a warning costs money every year, on each platform, and that money has not been paid. so:
- windows says it protected your pc. press more info, then run anyway.
- the mac dmg says the developer cannot be verified. open system settings, go to privacy and security, scroll down, press open anyway. the two lines above do not hit this at all, and the difference is not a trick: a mac marks what a browser downloads and refuses to open it unchecked, and does not mark what you fetched yourself with a command you typed.
if you want to check what you downloaded before you unpack it, run
shasum -a 256 ~/Downloads/keeper.tar.gz and compare the answer against the
.sha256 file next to the download on the releases page.
you should not take that on trust from a stranger. everything keeper is made of is in this repository, the downloads are built by a script you can read, and every file has a checksum next to it so you can prove the one you downloaded is the one that was built.
- open keeper. it opens in your web browser. that is just how the screen is drawn. nothing is on the internet and the page is coming from your own computer.
- drag a folder of photographs onto the page. any folder. keeper opens the folder where it already is and does not copy or move anything.
- wait once. keeper makes a small copy of each picture so the page can show hundreds at a time without crawling. a few thousand photographs takes a few minutes. it only ever happens once per folder.
- the shelf appears. every photograph in the folder, on one screen, and eight cards walk you round it. they say what the keys do, they run once, and skipping them is fine.
next time you open keeper it opens the same folder again. the question mark in the top corner holds every key and the settings, and the walkthrough is in there if you want it a second time.
move with the arrow keys. press k to keep the one you are on. press space to see it big.
that is the whole loop, and it is meant to be done with one hand without looking down. a thousand photographs is about twenty minutes this way and an afternoon with a mouse.
delete does not delete. press it and the photograph disappears from the shelf and the file does not move. it goes to keeper's own bin, which is a list, not a folder. you can put it back.
there is one button that really removes a file. it lives inside the bin, it asks first, it tells you how many, and it uses your computer's own delete, so the files land in the trash or the recycle bin and you can still get them back from there.
this is the second version of that and the first one was wrong. delete used to go straight to the trash. putting the fastest key in the app on the one thing you cannot undo is how a tool eats somebody's photographs.
instagram wants a tall picture. a youtube thumbnail wants a wide one. a banner across the top of a website wants something wider still. the same photograph does not survive all three, and you cannot tell which ones it survives by looking at it.
so click a photograph and keeper shows it in every shape at once.
drag it into the shape you want, drag inside that box to move the picture,
hold option and scroll to zoom in. press export and the cut picture lands
in your downloads folder, in a folder called keeper. the original is not
touched.
the shapes are already there on the first run: instagram post, square, story and reel, x, youtube thumbnail, link preview, pinterest, and a few sizes of web banner. each one is the size that platform actually wants.
if you zoom in too far, keeper tells you before you export. past a certain point there are not enough pixels left to fill the shape, so somebody else's website would stretch it and it would look soft. keeper compares the two and says so in red.
a pile is what you build while you browse. click photographs into it, keep going, start another one when the subject changes, then export that pile as a real folder of real files you can hand to anyone.
exporting never moves your originals. keeper reads from your folder and writes somewhere else. your folder comes out of it byte for byte identical.
clips sit on the same wall as the photographs, with a poster frame cut a third of the way in, because frame one is a lens cap or a whip pan often enough not to be worth trusting.
the posters actually appear now, and they had not in any release from
v0.6.1 to v0.8.0. keeper wrote each still to a temp name ending .part,
ffmpeg picks what it is writing from the extension, .part is not one of
them, and it refused before it decoded a frame. the error then put the tile on
the branch that means a file cannot be read, so keeper told people their
footage was corrupt when the only broken thing was a filename.
with the sorting below turned on, a clip is grouped like a photograph, off a handful of frames spread across it. that is enough to put it in a group and no use at all for finding the good eleven seconds of a four minute take, which is what moments is for. there is a button under a clip that says find the moments in this clip: keeper finds the real cuts, and what comes back is a row of timestamps you can jump to. it is one clip at a time and never automatic, because it decodes the whole file.
what a clip says is the other half, and it is a separate yes. the frames are enough on their own: "find the moments in this clip" works with no key and nothing sent anywhere, off the frames alone. sound is people talking, whole, so before any of it goes out a card names the service it would go to, says how long the sound is and what it costs, and asks. yes covers this archive until keeper is closed and is never written down. two services are wired, deepgram and assemblyai, each with its own key in this machine's keychain, and a moment that came from the words is marked apart from one that came from the frames, because they are worth different amounts of trust.
film needs ffmpeg and keeper cannot ship it: every static build anybody
actually publishes is licensed in a way that does not sit with the licence
keeper ships under. so keeper fetches it instead, on the same one yes as the
downloads tab, from a github release into its own folder, and never
redistributes it.
a copy already on your machine wins. keeper looks on the path first, then
in the folders ffmpeg is actually installed into, and only then at the one it
fetched. that middle step is there because a mac app opened from the dock
inherits a path with almost nothing on it, and the ffmpeg somebody installed
themselves is invisible to everything keeper spawns without it. keeper doctor says whether it has one.
this is off until you turn it on, and with it off keeper is exactly the tool described above. no request is made, nothing leaves your folder, and the wifi test below still passes. if that is the keeper you wanted, the rest of this section is not something you have to opt out of.
with it on, keeper sorts the archive itself, from the analyse tab beside the shelf and the bench.
you write the groups. up to nine, each a name and one sentence of what you
are looking for. that sentence is not a note for you to read later, it is what
the model is told to look for, and there is nowhere else it is told anything.
the digits 1 to 9 are those nine groups, and they do not move again for the
life of the folder.
nine, and not ten, because a keyboard's number row ends in a 0 that a finger
reaching for 9 finds by accident.
there is no fixed vocabulary any more. keeper used to ship sixteen letters
meaning portrait, laughing, presenting and food, which described a conference
and described nobody else's shoot at all. a wedding has no presenting, a
building survey has no laughing, and both have three things that list had no
letter for.
then two passes, and only the cheap one runs over everything. the first lays your photographs out as contact sheets, which are big grids of small pictures sized so a machine can still tell one face from another, and every frame comes back in one group or in none of them. the second is the expensive one, so it only runs over the groups you tick, one frame at a time, and it writes a sentence and a few words about each that the search box can then find.
if a sheet comes back miscounted, keeper refuses that whole sheet rather than applying it. one bad count would shift every label after it onto the wrong photograph, quietly, which is worse than no labels at all.
what leaves is written on the card before you say yes: the contact sheets, and the group names and sentences you typed. the request carries the instruction that says how to answer along with them, and that is the whole of it: no file names, no folder names, no dates, no location, and never an original. one address, the one you picked, and nothing goes to the other one.
there are two addresses on that card and never both: a model running on your own machine, which is offered first, and the claude api, second.
it says what a run costs before you start it. the line under the buttons
reads 126 frames, 6 sheets, about 0.02 dollars, and it counts what is about
to be sent rather than the size of the folder, so a folder already sorted
answers with the frames nobody has covered yet.
sort again is there for when the groups you wrote turn out to be the wrong groups. it says first, before you press it, that it writes over the group every frame already has, including the ones you put there by hand with the number row. it is off every time the tab is drawn.
changing the list tells you what it will do, with the count, before it does it. delete a group that forty frames are filed under and keeper says so: they keep the digit, they are not untagged, and that digit stops meaning anything until you write a group under it again. rename one and the frames stay where they are and wear the new name. nothing is migrated behind you, and a digit with frames on it and no group written keeps its own row on the shelf, so those frames are still one click away.
you can point it at a model on your own machine. the endpoint is a box you type a url into, so ollama, lm studio and anything else speaking that shape work here without keeper holding a list of them that would go stale. on that setting nothing leaves your machine at all.
and that path is slow, which is said here rather than left to be found out. this readme used to promise tagging without an assistant, and that it would ship only if it could be done locally, for free, and well enough to trust. locally and free are met. the speed is not: the only published number is an m4 max with 128gb of memory spending 1.5 to 4 seconds per picture on the vision encode alone, before the model has thought about any of it, and that is a better machine than most people are sitting at. a couple of thousand frames is most of an hour at the good end of that. so the local path is real, it works, and it is the slow one. the api is the one that answers in minutes and it is the one that sends something.
your key is not written into your archive. it goes in this machine's own
keychain, and on a machine with no keychain into a file in keeper's folder
that only you can read. a .keeper folder travels with the photographs, onto
other drives and into other people's backups, and a credential does not belong
in there.
the coding agent path still works, if you would rather use an assistant you
already pay for than a key of your own. keeper sheets writes the sheets out,
AGENTS.md is the brief you hand the agent along with your own
group list, and keeper tag takes the answer back.
the search box in the toolbar takes plain words, matched against the file name, the folder, and anything the second pass wrote. it also takes a few prefixes:
date:2026-07 camera:canon lens:50mm place:harbour
name:0034 group:3 group:harbour has:note
has:date is:kept is:binned is:film is:still
two words mean both of them. quotes hold a phrase together, because
place:"long table" is one question and two loose words are not. a prefix
keeper does not know is not an error: somebody typing colour:red has said
perfectly clearly what they are after, so the word is searched and the prefix
is dropped.
the date, the camera and the lens are read out of the file itself, and a file that carries none falls back to its own date on the drive. nothing is written for the search: the rows are built in memory when keeper opens the folder, and built then rather than when you start typing, so the first search is as quick as the tenth.
keeper opens a folder of photographs and film, and a piece of work often needs a piece of music as well. the downloads tab takes a youtube or spotify link and saves what is behind it into a folder you pick.
you choose what it saves as. mp3 because it plays in everything, m4a for the same quality in a smaller file, the original audio with no re-encoding at all, or the video, as an mp4. and how good: max is not a preset, it is whatever that link actually offers, so a 4k source comes down as 4k and a source that only has 720 comes down as 720. the two below it trade quality for a smaller file, in that order. what you pick is remembered.
it is off until you turn it on, and turning it on is a card that says what that costs before any of it happens: the internet, and programs keeper does not ship. yt-dlp and spotDL do the fetching, and ffmpeg turns what comes down into the file you asked for. say yes and keeper gets them from their own github releases, once on this machine, into its own folder. say no and none of it happens and it does not ask again.
that one yes is also what gets film working, because ffmpeg is what reads a clip at all.
a spotify link is a name rather than a file. spotdl is the one thing here that can turn that name into the track it stands for, and that is all it does: the download itself is yt-dlp both times. it is the half that has to stay current, so it is the half keeper keeps current.
one track a link. a playlist link gets the track it points at rather than the playlist, so a link pasted out of habit cannot start two hundred downloads.
what you may do with what comes down is between you and whoever made it. keeper does not check and does not know.
it is the first thing to check about anything you point at your photographs, so it is written out rather than claimed in a badge.
with the analysis off, nothing of yours leaves this computer, and it is off until you turn it on. no upload, no account, no login, no key, and no licence check. it works with the wifi turned off, and you can test that: turn the wifi off and use it. the shelf, the bench, the groups you type, the crops, the exports and the search all work with it off, and none of them ever make a request.
that sentence used to read "your photographs never leave your computer" with nothing after it. it was true then, and it is true now for anybody who leaves the analysis alone, which is how it arrives. it stops being the whole story the moment you turn the analysis on, so the rest of it is written out here rather than left in a promise that has quietly gone out of date.
with the analysis on, this is all of what goes out. downscaled contact sheets, meaning your photographs at a few hundred pixels each in a grid, the group names and sentences you wrote, and the instruction that says how to answer. no file names, no folder names, no originals, and nothing about you or your machine. it goes to the one address you picked and to no other, and if you pointed it at a model running on your own machine it does not leave the machine at all.
and none of it carries the block your camera wrote. the make, the lens,
the moment the shutter went and any gps that was on live in a frame's exif.
keeper reads that on this machine, which is how camera: and date: work in
the search, and it is not in the bytes that leave: every picture that goes out
is re-encoded on the way, the exif does not survive that, and a contact sheet
never had any to begin with. this was read off a real request rather than
reasoned about.
your key is kept out of your archive, in this machine's keychain, or in a file in keeper's own folder that only you can read where there is no keychain. somebody sent you a key file? drag it onto the keeper window, or pick it from settings, and the key goes into the keychain and the file can be deleted. keeper refuses a key file that sits inside an archive or a git checkout, and never says more than the last four characters of what it read.
nothing is counted or reported. no analytics, no crash reports, no usage tracking. not turned off by default, not in there at all.
four things can reach the internet and every one of them asks you first. keeper can check whether a newer keeper exists, and saying yes fetches one small file that carries no name, no photographs and nothing about you. the downloads tab, which is the only part of keeper that fetches something that is not keeper, and which now fetches ffmpeg on that same yes. the analysis. and the sound of a clip, which goes to the transcription service you picked and only when you say so for that clip or that archive, an answer that dies when keeper closes. each of the four is a card that asks in words, before anything happens, and saying no to one means it never asks again and never makes the request.
your photographs are read and never written to. keeper makes one folder
inside yours, called .keeper, and puts its small copies, your groups, your
labels, anything the second pass wrote and your crops in there. delete that
folder and your archive is exactly as it was.
nothing is generated. no picture is made here and nothing is painted into one of yours. the model is asked to read, and it answers in words: a digit, a sentence, a few keywords. not one pixel of anything you shot is changed by any of it.
keeper is a side project. this is what it is pointed at, not a schedule, and it is written down so you can tell whether the thing you need is on the list or not on it at all.
- cutting a clip to a shape, and trimming it. a clip gets a poster, a group and its moments now. the bench still crops stills and nothing else.
- more inside the crop. the bench gives a slot one crop and stops there.
- an export that goes where it is going, instead of landing in a folder you then have to open somewhere else to post it.
- what a clip says, without sending it anywhere. the words come off a service now, on a yes that names it. speech recognition that runs on your own machine would make that yes unnecessary, and this line stays until an ordinary machine can do it in a time somebody will sit through.
- an analysis that is local and fast. the local endpoint exists and works and is slow, for the reason written out above. this line stays here until an ordinary machine can do the cheap pass in a time somebody will sit through, and apple's on device vision framework is the thing worth watching for it.
none of that is promised for a date. it gets built when it gets built.
keeper can check itself over and tell you what is missing.
on windows there is a keeper doctor shortcut in the start menu, next
to keeper itself. on a mac it needs a terminal: keeper doctor.
either way it answers in plain sentences rather than error codes, and that is the thing to copy and send on when you ask anyone for help.
on windows, keeper cannot read raw camera files on its own, because windows does not come with anything that reads them. ffmpeg reads most of them, and the downloads card is where you say yes to keeper fetching it. jpg, png, heic and the rest work either way. macs read everything already.
everything above works without one. this part is for people who want it.
npm install
node bin/keeper.mjs ~/Archive
keeper <folder> scan, thumbnail, and open the shelf
keeper app [folder] the way the icon opens it: remembers the last
archive, takes its own port, and reuses the
copy that is already running
keeper sheets <folder> contact sheets for a coding agent to read
keeper tag <folder> <file> apply the tags that agent wrote
keeper export <folder> write the placed crops out
keeper trays <folder> what is in the trays, and how much
keeper init [folder] create keeper.config.json
keeper doctor what this machine can and cannot do
--port defaults to 7777, --cols and --rows set the contact sheet grid at
6 by 4, --rescan rebuilds an index that already exists, and --no-open
leaves the browser alone. typed bare, with no folder after it, keeper
prints this list: a terminal opens in your home folder, and thumbnailing
every file you own is not what someone asking what the command does had in
mind.
| arrows | move the cursor, and a digit puts the frame it is on in that group |
k |
keeps the frame |
space |
quick look, the way it works everywhere else on this machine |
| click | picks, shift+click a range, cmd+click one, cmd+drag a box |
cmd+a |
everything the filters left, and not one frame more |
| a digit, with frames picked | groups all of them at once, and moves nothing |
cmd+return |
sends the picked frames to the tray |
option+r |
reveals the original in finder, selected |
cmd+f |
finds, option+o opens another folder, option+1 and option+2 change view |
delete |
sets the frame aside in keeper's bin, nothing on the drive moves |
the modifier is option and not cmd on three of those because chrome resolves
cmd+r, cmd+o and cmd+1 above the page: preventDefault runs and
the tab reloads anyway. the physical key is read off e.code rather than
e.key, because option on a mac rewrites the character and option+r
arrives as ®.
the built in formats live in src/formats.mjs. your own go
in keeper.config.json:
{
"slots": [
{ "id": "hero", "aspect": "16/9", "width": 2400 },
{ "id": "about-1", "aspect": "3/2", "width": 1200,
"note": "the room rather than a wall of faces" }
]
}the config is read from the folder you run keeper in, not from the archive,
because the holes belong to the project and the photographs do not. keeper init writes the example there for you. a slot of yours with the same id as a
built in one replaces it quietly, and "formats": false turns the whole
standard set off.
if you have not zoomed in and you have a config, the bench also prints the
exact object-position line for your stylesheet, so the original ships uncut
and the browser does the cropping.
keeper trays ~/Archive
keeper trays ~/Archive --export tray-1 --to ~/Desktop/for-the-site
keeper trays ~/Archive --export tray-1 --to ~/Desktop/live --mode symlink
copies are real files, yours to hand to anyone. symlinks and finder aliases copy nothing and point at the originals, the first breaking if you move them and the second following them. the terminal names the mode it just ran every time, because a folder of links and a folder of copies look identical in a listing and weigh nothing alike.
keeper refuses to export into a folder inside the archive. it would work once. the next scan would find the copies, give them their own ids, thumbnail them, and you would be browsing every exported frame twice with the tags on only one of the pair.
the server binds to 127.0.0.1. not 0.0.0.0, which means it cannot be
reached from the other laptop on your own wifi, never mind from outside. one
browser, one machine. every fetch in the source names a path on this
machine, github, or the analysis endpoint you chose yourself, and
grep -rn "fetch(" src web bin is still the whole audit.
a placement is stored in the negative's own coordinates, not the screen's. three numbers: where the centre of the crop sits in the source, and how wide it is as a fraction of the source. the height falls out of the slot's aspect at the moment of painting. that is what lets a layout change a frame's aspect at a breakpoint without silently meaning something different. a placement written in screen pixels would.
a frame's id is a hash of its path, not its position. drop 200 new photographs into the archive and every tag written last month still points at the same picture. position based ids would have slid by 200 and repointed the lot, quietly. it is also why a frame you take back out of the bin comes back to its own tags.
a browser is never told where a dropped folder lives. it gets the name,
and for a folder it may list what sits directly inside, and that is the end of
it. so keeper recovers the path three ways: the drag's own file:// url when
the source writes one, the machine's search index, and a read of the handful
of folders an archive is ever kept in. if all three come up empty the real
folder dialog opens, which is the only thing on the machine that can hand a
browser a path and mean it. nothing in it ever says the drop failed when what
happened is that the browser withheld the path.
the two machines differ in five places and only five: the file manager,
the wastebasket, the shortcut a tray exports as, the search index, and the
list of folders to look in. those live in src/os/ and everything above them
is one codebase.
reads jpg, png, webp, avif, tif, heic and dng, and mov, mp4, m4v, mkv, avi,
webm and mts. raw files come in through their embedded preview, which is all a
contact sheet needs. clips need ffmpeg, which is looked for on the path,
then in the folders it is actually installed into, then in keeper's own, and
fetched into that last one if you said yes. the middle step exists because a
mac app opened from the dock inherits a path with no homebrew on it, and a
tester's own ffmpeg is invisible to everything keeper spawns without it.
keeper can update itself, and it asks before it ever looks. what comes down is about a quarter of a megabyte, because it is keeper and not the runtime under it. it is checked against the checksum the release published before a file moves, the copy being replaced is set aside rather than deleted so a failure puts it back, and a release that changes what keeper depends on cannot be installed this way, says so, and sends you to the downloads instead of installing half of something.
on a mac the app is signed again after the swap, with the same ad hoc signature the build gave it, and the quarantine mark a browser download carries comes off. without both, an app that updated would be refused as damaged the next time it opened, which is what happened to every keeper before 0.9.3: the update landed and the app never came back. one of those will tell you the newest keeper needs a full download and offer the downloads page. it is right: get the dmg once, and it updates itself from there.
everything keeper writes goes in <archive>/.keeper/: the index, the small
copies, the contact sheets, the groups you wrote, your tags, the sentences the
second pass wrote, your placements and your trays. delete that folder and the
archive is exactly as it was. the groups live with the photographs and not
with keeper, because they are a fact about this set of photographs: copy the
folder to another machine and the names of the labels travel with the labels.
opened from its icon it keeps a little more, and this is all of it: which
archive was open last, which port it is on, what you have answered about
updates, downloads and the analysis, and which model and endpoint the analysis
is pointed at. ~/Library/Application Support/keeper on macos,
%LOCALAPPDATA%\keeper on windows. an api key is in neither of those files:
it goes in this machine's keychain, and where there is no keychain, in a file
beside them that only you can read. if you said yes on the downloads card,
yt-dlp, spotdl, ffmpeg and ffprobe sit in a bin folder beside those, which
is also why an update to keeper never has to fetch them again. deleting the
lot loses the memory of where you were, and means they are fetched again next
time you ask for them.
a red square and the word keeper beside it, and a photograph is the only
thing on the screen worth looking at.
two zones, and the split is the whole rule. anything that touches a
photograph is strictly equal rgb: #0b0b0b for the wall, #101010 for the
plate under a frame, black for quick look. a warm cast beside a picture lies
about the picture. chrome that never borders one is a whisper warm, #151310
for bars and panels and #1d1915 above them, and there is one warm ink,
#f2efe9, at three strengths.
#e1062c is the accent, and it is a fill or a ring and never a word: red as
text is #ff5d75. amber #e0a458 means deal with this and green #4fc978 is
quiet confirmation. the accent means one thing anywhere it appears, which is
that you chose this. a tab you are already looking at is not that, and neither
is a photograph.
type is geist and geist mono, self hosted in web/font, ofl, nothing fetched
at runtime. the mono carries the wordmark, the nav, every label, every chip
and every number, and the sans is left to do the one thing a mono cannot,
which is read as a sentence.
source available, not open source. free while keeper is in testing, and it is
in testing now: use it for anything, at home or at work, on as many machines
as you like, no payment, no account, no key and no licence check.
LICENSE says it in words rather than in a badge.
what you cannot do is sell it, redistribute it, or run it as a service. later versions may be sold under a one time licence, and every version published while keeper is in testing stays free under this licence forever, so a copy you already have never stops working and never has to be paid for.
it was mit, then apache 2.0 while there was nothing to sell. those releases
are still apache and that cannot be taken back. a dmg is not source: it
carries node, sharp and libvips, and libvips is lgpl, so shipping that binary
means saying whose work is inside it and letting somebody swap it.
NOTICE does both and names the file, and nothing in keeper's own
licence takes away anything those licences give you.






