Insomnia
Keep your Mac awake. Give the session an end time.
A native macOS menu bar app for timed awake sessions. Pick a duration for your long-running work, choose what happens when the lid closes, and see when recovery needs your attention.
Install · Using it · Recovery · Security
Use a stable, well-ventilated surface—not a closed bag. Insomnia is
experimental software. Recovery can fail; a running timer is not a safety
guarantee.
Validation status · Apple's ventilation guidance
Install
Requires macOS 26 or later on an Apple Silicon Mac.
Insomnia is published on two channels on the releases page:
- Stable: the release GitHub marks Latest, tagged
v. Use this one
- Nightly: prereleases tagged
nightly-, built automatically-
main once a day when it has new commits. Newest code, experimental,
and possibly broken. A nightly is never marked Latest.
Each release's notes give the exact commands for that release. One release
differs from the steps below: v0.1.0, the first stable release, was packaged
by hand from the maintainer's installed app, not by the Release workflow. It
has no attestation, and its zip installs with
./scripts/install.sh --app ./Insomnia.app. If the release you picked is
v0.1.0 (its zip is Insomnia-0.1.0-macos-arm64.zip), follow the install
steps in its release notes instead of steps 2 and 3.
The rest of this README describes releases built by the Release workflow and
builds of the current source, not v0.1.0. The README.md in the v0.1.0
zip describes what that release installs, how its recovery works and its
limits. In particular, the installer's version check, the recovery script
sealed inside the app and the code requirement the recovery agent pins,
described below, are not in v0.1.0.
Paste this into your coding agent:
Install the latest stable Insomnia release from https://github.com/krishhgg/Insomnia by following its README. If a step needs my password, give me the command to run in Terminal.
For the newest nightly instead, write "the newest Insomnia nightly prerelease" in place of "the latest stable Insomnia release".
Or run it yourself (gh is the GitHub CLI):
- Download the zip and
SHA256SUMSof the release you chose into an empty
gh. For the stable release, the
one marked Latest:
gh release download --repo krishhgg/Insomnia --pattern '*.zip' --pattern SHA256SUMS
For the newest nightly, paste this whole block. It reads every page of
the release list and picks the most recently published nightly. It
downloads nothing, and returns an error, when it cannot read the list,
when no nightly has been published yet, or when the tag it picked is not
in the nightly- form. It never falls
back to the stable release.
insomnia_nightly() {
local list tag
list="$(gh api --paginate 'repos/krishhgg/Insomnia/releases?per_page=100' \
--jq '.[] | select(.prerelease and (.draft | not) and (.tag_name | startswith("nightly-")))
| "\(.published_at) \(.tag_name)"')" ||
{ echo "Could not read the release list. Nothing was downloaded." >&2; return 1; }
tag="$(printf '%s\n' "$list" | sort | tail -n 1)"
tag="${tag#* }"
if [ -z "$tag" ]; then
echo "No nightly has been published yet. Nothing was downloaded." >&2
echo "Wait for the next daily build, or build from source." >&2
return 1
fi
if ! printf '%s\n' "$tag" | grep -Eqx 'nightly-[0-9]{8}-[0-9a-f]{12}'; then
printf 'Unexpected nightly tag: %s. Nothing was downloaded.\n' "$tag" >&2
return 1
fi
gh release download "$tag" --repo krishhgg/Insomnia --pattern '*.zip' --pattern SHA256SUMS
}
insomnia_nightly
If the releases page has no release yet, build from source (below).
refs/tags/v for a stable release or refs/heads/main for a
nightly. is the full 40-character commit the release was built
from: its release notes name it after "Built from" and carry this command
filled in. A nightly's tag ends with the first 12 characters of it.
shasum -a 256 -c SHA256SUMS
gh attestation verify <zip> -R krishhgg/Insomnia \
--signer-workflow krishhgg/Insomnia/.github/workflows/release.yml \
--source-ref <ref> --source-digest <commit>
The first command checks the zip against the SHA256SUMS downloaded with
it. The second checks that this repository's Release workflow built this
exact zip from that commit for that ref, so a nightly cannot pass for a
stable release. Every nightly is built for refs/heads/main, so only
--source-digest keeps another nightly's zip from passing: do not leave
it out, and do not shorten the commit to the 12 characters in the tag.
- Unzip and run the installer that comes in the zip:
ditto -x -k <zip> .
cd <the zip's name without .zip>
./install.sh --allow-unverified-origin --app ./Insomnia.app
open "$HOME/Applications/Insomnia.app"
Releases are ad-hoc signed and not notarized. The installer can check that
the bundle is intact but not who made it, so it refuses to install without
--allow-unverified-origin, which says you ran the two commands in step 2.
macOS blocks the first launch of a downloaded copy until you allow it in
System Settings > Privacy & Security.
The installer checks the bundle's signature, identifier and version before it
asks for anything (the v0.1.0 installer checks the signature and
identifier, not the version). It then installs the app and a background recovery agent,
and asks for administrator access to install a narrowly scoped sudoers rule. It grants
your user account, not just Insomnia, passwordless access to four
power-setting commands. Review that permission before installing.
Release zips are built for arm64 only, and their install.sh --app stops on
an Intel Mac. On Intel, building from source (below) is the only option, and
it is untested there.
Build from source (experimental)
Requires Xcode with Swift 6.2 or later. Clone the latest stable tag, or a
nightly tag for the newest code, rather than main. If the releases page has
no release yet, leave out --branch v to build main:
git clone --branch v<version> --depth 1 https://github.com/krishhgg/Insomnia.git
cd Insomnia
./scripts/install.sh
open "$HOME/Applications/Insomnia.app"
scripts/install.sh builds the same bundle the release workflow builds
(scripts/build-app.sh), ad-hoc signed, and installs it the same way. It
builds only when it runs from a checkout's scripts folder, with
Package.swift one level up, and then runs the build-app.sh beside it. The
install.sh from a release zip stops and asks for --app instead, even when
a build-app.sh was added to its folder after unpacking. A checkout of
v0.1.0 has the installer of that release; its README describes it.
docs/releasing.md describes the release pipeline.
Exactly what gets installed
This describes releases built by the Release workflow and builds of the
current source. v0.1.0 installs an older layout. Its app has no
backstop.sh inside it: its installer copies backstop.sh into
~/Library/Application Support/Insomnia/, and its recovery agent runs that
copy with /bin/bash without checking any code signature. The sealed script,
the pinned code requirement and the refusal of an edited or re-signed bundle
described below do not apply to it, and neither does the upgrade procedure.
Its sudoers rule grants the same four commands. The README.md in its zip
describes what it installs and its limits.
| Location | Purpose |
| --- | --- |
| ~/Applications/Insomnia.app | The menu bar app, with backstop.sh sealed inside it at Contents/Resources |
| ~/Library/Application Support/Insomnia/ | Configuration and the session/recovery journals |
| ~/Library/LaunchAgents/com.insomnia.backstop.plist | Per-user recovery agent: verifies the app's code signature, then runs the sealed backstop.sh |
| ~/Library/Logs/Insomnia/ | insomnia.log and handoffs.log, each capped at 1 MiB with one older copy kept as .1, unless you replace it with a symlink |
| /etc/sudoers.d/insomnia | Permission for the four commands below |
/usr/bin/pmset -a disablesleep 1
/usr/bin/pmset -a disablesleep 0
/usr/bin/pmset -b lowpowermode 1
/usr/bin/pmset -b lowpowermode 0
The grant is available to other processes running as your user. Insomnia is not sandboxed. The app, scripts, and journals are local; hotspot passwords use the login Keychain, not the configuration file.
The recovery agent runs at login and every 60 seconds. Its command line pins
the installed bundle's code requirement (for an ad-hoc build, the cdhash of
that build) and runs codesign --verify --strict against it before executing
the backstop.sh sealed inside the bundle. An edited bundle or script fails
that check: the agent writes one line to insomnia.log and runs nothing until
you reinstall. So no other account can edit it, the installer removes group
and other write permission and every ACL from the bundle it installs. The
signature covers neither, so the bundle still verifies, and extended
attributes such as a download's quarantine flag are kept. No executable is
kept in a writable support directory. The plist in ~/Library/LaunchAgents
is still a per-user file that any program running as you can edit, like
every LaunchAgent; the app rewrites it at the next session start when it does
not match, which is a repair, not a tamper check.
What the app pins is the requirement of the code it is itself running, read
through the Security framework after checking that the bundle on disk is still
that code and still passes the agent's check. A bundle whose sealed script was
edited, or that was re-signed under the running app, is refused rather than
pinned: the app does not start a session, or reports the end as incomplete,
and names the reason, until you reinstall. With ad-hoc signatures this guards
against accidental edits and against the app relaying a tampered bundle into
the agent, not against a process running as you: that process can edit the
plist, load its own agent, quit the app and launch a replacement, and run the
four pmset commands itself.
An upgrade asks the running app to quit and stops if it refuses. The new
bundle is built in a staging directory next to the app and moved into place in
the same step that replaces the recovery agent. That step starts only after
launchctl print confirms the previous agent is unloaded; otherwise nothing is
replaced. If the new agent cannot be loaded, or its plist cannot be saved, the
installer unloads it, waits for launchctl print to confirm that, and puts the
previous bundle back, so the loaded agent always matches the installed app. A
bundle that cannot be moved during that step is handled the same way: the
previous bundle goes back and its agent is loaded again. If the previous bundle
itself cannot be moved back, nothing is deleted: it stays at
~/Applications/.Insomnia.app.previous, the installer prints the two commands
that put it back and load its agent, and until then the agent finds no app, so
run them or rerun the installer before you log out. If
the unload is not confirmed, the new bundle stays with the agent that pins it
and the installer asks you to rerun it. After that, or after an install killed
in the middle of that step, the next run keeps whichever bundle the agent's
plist on disk pins. It does that only after its own recovery step succeeds:
while recovery is unresolved, an agent the earlier run left loaded may be the
one retrying it, so the installer stops without unloading that agent or
moving either bundle. Once recovery succeeds, it unloads that agent, moves the
bundle back and loads the plist on disk again, and it stops if launchctl
print does not confirm the unload or the reload. Unresolved recovery prevents
replacing either; follow the reported instructions before retrying. The
installer checks the sudoers rule again once it holds the recovery lock, and
stops if the rule is gone, as after an uninstall.sh that took the lock first.
Each call it makes to sudo, pgrep, launchctl or codesign while it
holds the lock has a 30 s limit, which a supervising process enforces even
if the installer is killed meanwhile. The call keeps the lock until it has
exited or been stopped, so no launchctl bootout it started is still
running once the lock is released. A call that does not answer in time gets SIGTERM, then SIGKILL
one to two seconds later, and the install stops, so the lock is released and the app
and the agent's backstop can take it again to undo a session. sudo only
ever gets SIGTERM: one that ignores it keeps the lock until it ends, and the
installer prints its pid.
Using it
- Start: click the eye in the menu bar, enter Days / Hours / Minutes, and
- Extend: click the eye or countdown during a session and enter more time.
- End early: press and hold the end control beside the countdown.
- Inspect or configure: right-click for status, recovery warnings,
You do not need to close the lid to use a timed session. Opening the lid does not end it, and a sleeping display is not the same as a sleeping Mac.
Before the first session, review the settings. Some lid actions are on by
default, including pausing the apps on the freeze list (Slack, WhatsApp and
Discord) while the lid is closed; pausing every other Dock app is off until
you turn it on. Start with a short, supervised session on a ventilated surface
and check the status menu and ~/Library/Logs/Insomnia/insomnia.log afterward.
What happens when the lid closes
During a session, Insomnia turns the display and keyboard backlight off (saving their brightness first), pauses the apps on the freeze list (and, if you opt in, every other Dock app that is not an agent app), checks whether Docker Desktop is idle before pausing it, and saves then mutes audio. Reopening the lid attempts to undo those lid actions. If the lid opens while Insomnia is still checking Docker, Docker is left running and the undo starts right away. **The timer keeps counting down while the lid is closed**; only its on-screen redraw pauses, also for a session started with the lid already closed.
The display step exists because the sleep guard stops macOS from doing it: with sleep disabled, closing the lid no longer turns the panel or the keys off by itself. Insomnia sets both to zero and restores them when the lid opens. Both go through private macOS frameworks. The display calls run only on a macOS major version they were measured on (26). The keyboard calls run only while the private keyboard class has the method signatures measured on 26, on whatever version. If either check refuses a device, Insomnia leaves it alone and Settings says why under the toggle. A level saved before an update that the check now refuses stays saved for a version that can restore it, and the menu says to set it with the brightness keys meanwhile. That version leaves a level you set by hand alone, and decides only on a reading taken with the display awake and the keys not dimmed. Once Insomnia's own Low Power Mode has been on over the saved display level, it leaves that level undecided until the Mac restarts, through relaunches of the app, and until then a lid close leaves that display lit and only asks it to sleep. The same goes when the recovery agent switches that mode off before Insomnia starts again, and when Insomnia switches off a mode still claimed from before a restart. That mode may read off then, yet it may have gone off only a moment before, so the level waits for the next restart even when the mode has been off for days. Once it has seen that display lit above zero, it never writes the saved level over a zero you set by hand, also after a relaunch or a restart. It cannot tell that zero from one auto-brightness left under a closing lid, so the saved level stays undecided, with nothing written, until you raise the display above zero. If Insomnia cannot record that it saw the display lit, or that the saved level is settled, Quit waits until it can. The display comes back to the brightness sampled while the lid was open, not the reading at the moment of closing (auto-brightness has already dimmed the panel under the closing lid by then, and Low Power Mode rescales it), and if Insomnia's own Low Power Mode was on while the lid was closed the value is written once more when the mode ends. If Insomnia is not running when you open the lid, press the brightness-up key.
The defaults are worth knowing:
- Selected apps: Slack, WhatsApp, and Discord are on the freeze list.
- Every other app: "Freeze every other app while the lid is closed" is
- Meeting, recording and dictation apps: never frozen by "Freeze every
- Docker rule: off. Turn it on to pause Docker Desktop on lid close when
docker ps or a timeout at either point leaves Docker running. A container
that starts between the second check and the pause is still paused with
Desktop, so leave the rule off for Docker workloads an unexpected pause
would hurt.
- Mute on close: on, so sound stops when the lid closes. Lid open
- Microphone: on Mac laptops with Apple silicon or a T2 chip, closing the
- Display and keyboard backlight: on ("Turn off the display and keyboard
- Low Power Mode while the lid is closed: on. With sleep disabled a closed
- Battery rules: below 40% on battery, request Low Power Mode; below 10%,
config.json with
the floors out of order is corrected at launch, and logged, by raising the
Low Power Mode floor.
Upgrading from an earlier build changes two of these once. On the first
launch of this version, a config.json saved by an earlier build gets "Freeze
every other app" turned off and "Mute audio on lid close" turned on, and
Insomnia posts a notification naming what changed. Settings shows the same
line at the top of Lid-close actions until you dismiss it, for anyone with
notifications off. The toggles are right below it. config.json records that
the update ran (lidCloseDefaultsApplied), so a setting you turn back stays
the way you set it. A fresh install starts with the new defaults and no
notice.
To exercise the lid actions without closing the lid, run
scripts/simulate-lid.sh closed and then scripts/simulate-lid.sh open during
a session; the app runs the same actions it would on a real lid event. Only a
build with the file watcher compiled in reads that trigger: a debug build, or
a release build installed with INSOMNIA_LID_SIMULATION=1 ./scripts/install.sh.
A normal install has no watcher, so no program running as your user can replay
the lid actions by writing a file. A build that has it logs "Lid simulation
build" at launch and shows the same line in the status menu and in Settings.
How recovery works
Insomnia records pending changes in a recovery journal. On session end, the app
attempts to undo them. An independent launchd agent checks every minute and
can attempt recovery after the app exits unexpectedly, once the saved deadline
has passed. It leaves a valid, unexpired session alone.
The app and backstop use the same lock so they do not restore and rewrite the
journal over one another. Failed restoration keeps the relevant entries;
unreadable journals are preserved instead of treated as clean. A session file
that does not parse counts as expired and is renamed to
session.json.unreadable- beside it, never deleting or overwriting
anything: the app does this at launch, before restoring whatever the journal
holds, and says where the file went; the agent does it once the journal is
clean. A session file that cannot be read at all (permissions, or not a
regular file, which is never opened) also counts as expired, since its end
time is unknown: the journal is restored and the file is renamed the same
way without being opened, so a later launch cannot resume a session that
was treated as ended. The app says where it went. If the rename fails, the
app keeps trying it and will not quit until the file is gone.
uninstall.sh --purge removes the renamed copies that are regular files;
without --purge they stay.
Recovery is not “everything always gets undone.” The backstop does not monitor battery or temperature. Saved audio needs the app to reopen, and unconfirmed process freezes may need manual inspection. If a warning remains, resolve it before leaving the Mac unattended. Real-machine crash, reboot, and installation scenarios still need release validation.
Recovery limits and manual attention
- Process ownership: automatic resume checks the recorded process start
- Identity is not an atomic guarantee: the app checks start time to the
Insomnia --resume-frozen) to do the same check and send the signal for
every entry that records microseconds, all such entries in one call with a
30-second limit. The entries go to the binary on standard input, so a long
journal cannot exceed the argument size limit. The backstop never signals those entries itself. It keeps
them when the binary is missing, does not finish in time, or answers
anything but one expected line per entry. It runs the binary only when the
installed bundle declares InsomniaResumeFrozenVersion in its
Info.plist, so it never starts an older build. The binary holds the
recovery lock while it can still send a signal and ends itself after the
same limit, so a backstop run that is killed mid-call leaves no helper
that could act later without the lock. uninstall.sh uses the backstop
installed with an app that does not declare that version. With no such
copy, the checkout's backstop keeps those entries and uninstall stops
before removing anything. Entries written by builds before
microseconds were recorded keep the one-second ps comparison in the
shell. A lookup and a signal are still separate operations, one pid at a
time.
- Stuck power commands: a
sudo pmsetthat has not finished after 20 s
sudo kill ; the
warning goes away when the command exits. The pid is also written to
unfinished-command.json with the command's start time and boot
session. A relaunch that finds the lock busy names the command in the
menu. It gives the pid and sudo kill, in one notification as well,
only while that pid still has the recorded start time and boot session;
otherwise it says the command has exited, since the pid may now belong
to another process. The relaunch tries again 30 s after each refusal.
Once the command has exited, it resumes a session that has not expired,
with its battery floors, and checks Low Power Mode the way it does after
its own command exits, described below. A session the user starts
before that next try gets the same check. Until the command exits,
Insomnia refuses to quit or start a session, and records any end or lid
event it refuses. A
disablesleep 0 or lowpowermode 0 that exits 0 counts as done: its
journal entry is cleared before the lock is released, and the command
is not run again. If that journal write fails, the menu says so and the
undo runs again; the line goes once a later write clears the entry. Any
other exit counts as a failure. Then a pending end runs again.
Otherwise Insomnia reads Low Power Mode. If it reads off, Insomnia runs
its own lowpowermode 0 and forgets the mode only once that succeeds.
Then it replays a refused lid event, after waiting out the 2 s lid
debounce, and runs the floor rules again. If the mode cannot be read or
switched off, or the journal cannot be written, it tries again every
30 s while the session lasts.
- Audio: the backstop preserves volume/mute entries but cannot restore
- Sleep disabled by something else: at launch, with no session and no
SleepDisabled 1 in pmset -g is left alone: Insomnia
did not set it and only its owner should undo it. The menu shows a warning
and a notification gives the command, sudo pmset -a disablesleep 0.
Ending an Insomnia session sets it to 0 whoever set it.
- Low Power Mode: Insomnia checks the existing setting so it does not
- App Nap: off by default. When the setting is on, Insomnia journals each
NSAppSleepDisabled value before writing it and puts
it back at session end, in the backstop, and in uninstall. Values an older
build wrote without a record are not guessed at: uninstall prints the
defaults delete command for each one and continues.
- Uninstall: refuses to remove recovery machinery while unresolved changes
Optional extras
iPhone hotspot handoff and tmux
Set System Settings → Wi-Fi → Ask to join hotspots → Automatically, then
enter the hotspot SSID and password in Insomnia Settings. The password is
stored in the login Keychain under service insomnia-hotspot. Insomnia uses
CoreWLAN to find and join that network without putting the password in process
arguments.
The Keychain item's access list names only the build of Insomnia that saved it, and Insomnia reads it with Keychain prompts switched off, so a join during an outage never raises a dialog. The installer signs each build ad hoc, which gives every install a new identity: after a reinstall the saved password is unreadable by the new build. Insomnia then skips the join, shows "Hotspot password unreadable by this build" in the right-click menu and in Settings, and sends one notification per outage. The warning belongs to the SSID it was read for. Change the SSID and it goes, and Settings checks the new SSID's saved password instead. Enter the password again in Settings and save; the save writes the new password before it removes the old item, and macOS may ask you to allow Insomnia to delete the old one, or to unlock the login keychain. If that save is cut off after the old item is gone, the password reads as missing and you enter it once more: Insomnia never reads a half-finished save's copy. The Save button reads "Saving…" until macOS answers, and "Saved" only while the SSID and password fields still hold what was saved. A join that was waiting while you changed the SSID is dropped, and the next retry uses the new SSID. A Settings read that was waiting is dropped too, and Settings reads the new SSID's password instead. Anything you type in the password field while Settings is still loading the saved one stays, even if you delete it again. The rest of Insomnia, including the battery floor and End, keeps running while the dialog is open. A build signed with a stable identity would keep the item readable across upgrades.
macOS requires Location Services permission to reveal network names. Insomnia requests it on the first hotspot save, or when starting a session with a configured hotspot—not merely on launch. If denied, use the Location row in Settings to open Privacy & Security → Location Services. Mac apps have no when-in-use grant, so System Settings records it as Location Services access for Insomnia. Insomnia uses it only to read Wi-Fi network names through CoreWLAN and never requests your location.
After a long outage (90 seconds by default) Insomnia types continue into
each configured tmux target. The default target list is empty, and a listed
pane is only nudged if you have marked it yourself, with a pane option that
is read again before every send:
tmux set-option -p -t <session:window.pane> @insomnia-nudge on
Mark a dedicated, disposable agent pane, not one you type in, because pending
text is opaque to Insomnia. Enter is off by default, so the word is typed and
nothing submits it. Turn on "Press Enter after continue" in Settings to submit
it, knowing that Enter also submits anything already typed in that pane. The
option must be on the pane itself (-p). One set on the session or window
does not count. Pane options need tmux 3.0 or later. Ending a session cancels
pending automation but cannot retract keystrokes already sent.
Chrome, Chromium, and Arc throttling
Chromium browsers can throttle windows macOS considers occluded, including
when the lid is closed. Insomnia detects supported running browsers missing
--disable-backgrounding-occluded-windows or --disable-renderer-backgrounding
and offers Relaunch [browser] unthrottled in the right-click menu. The item
asks first, because the browser is quit and its windows and tabs come back only
if it is set to reopen them on startup. If the browser has quit by the time you
confirm, nothing is quit or launched and a notification says so. Insomnia reads the browser's profile
arguments before quitting and carries them over. If it cannot read them, cannot
read the kernel's start time that ties them to the browser, or the browser
quits on its own while they are read, it quits nothing and says so. If the
browser has not quit after 10 s, nothing is launched, and a notification says
so: a second copy beside the first would be worse than a throttled one. The
quit request stands, so a browser that closes later has to be opened again by
hand. After open returns, Insomnia waits up to 5 s for the browser to show up
as running and notifies if it does not. Each of these reasons also stays in the
right-click menu as a warning line, one per browser, until that browser's next
relaunch or the next session, so it is there even with notifications off. A
relaunch that ends after its session ended, or after a newer relaunch of the
same browser started, reports nothing. This is not a guarantee that every web
app will keep working while the lid is closed.
Configuration and privacy
Configuration lives in ~/Library/Application Support/Insomnia/config.json.
Use Settings for the app's controls; Config.swift
defines the full configuration and defaults. Local logs can contain SSIDs,
process metadata, and tmux targets. Check them before sharing publicly.
Lines the app writes to insomnia.log also go to the unified log with their
bodies marked private, so log show and other local programs see
in place of the text unless private data logging is enabled on the Mac. The
backstop's lines go only to insomnia.log, which keeps the full text of both.
The files in Application Support/Insomnia and Logs/Insomnia (config, session,
journal, recovery lock, the record of a power command left running, the two
logs) are owner-only, mode 0600 with those two directories 0700, and one left
looser by an older build is tightened the next time the app or the backstop
opens it. Insomnia sets only these modes and
leaves any access control list (ACL) on these files and folders as it is, so
an ACL someone added, or one inherited from a parent folder, can still give
another account access (ls -le shows it). The LaunchAgent plist and the installed
scripts hold no private data and keep the modes the installer gives them.
insomnia.log and handoffs.log are capped at 1 MiB: a
log past the cap is renamed to insomnia.log.1 or handoffs.log.1,
replacing the previous copy, and a new file starts. The cap does not apply
to a log you replace with a symlink. Insomnia writes through the link and
never rotates it, since the rename would move the link and not the file it
points to, and it logs that once. You set up the link, so trimming the file
it points to is up to you.
INSOMNIA_HOME relocates app support files, logs, and LaunchAgents for testing.
It is not an installation sandbox: installation/removal also involves the
app bundle and sudoers rule. The installer refuses a relocated home. See
Paths.swift for the layout.
Uninstall
From your checkout:
./scripts/uninstall.sh
Also remove Insomnia-owned configuration and logs:
./scripts/uninstall.sh --purge
From the unpacked release zip, run ./uninstall.sh (or ./uninstall.sh
--purge) in its folder (Insomnia-, or the nightly name). A
checkout's uninstaller (in scripts, with Package.swift one level up) runs
the backstop.sh beside it. Anywhere else, such as the zip's folder, the
uninstaller runs only the copy sealed in the installed app, after codesign
--verify --strict passes on the app, and stops without removing anything
when there is none. The zip has no backstop.sh, so one added beside its
uninstaller is not run. Neither looks in the folder above its own.
The v0.1.0 zip is different: run ./scripts/uninstall.sh in its unpacked
Insomnia-0.1.0-macos-arm64 folder, as its release notes say. That older
uninstaller runs the scripts/backstop.sh in the same folder.
The uninstaller requests cleanup before removing the app, agent, and sudoers
rule. If recovery is incomplete or the app refuses to quit, it stops; resolve
the reported problem and retry. With the app it removes what an interrupted
install left beside it: ~/Applications/.Insomnia.app.previous, and
.Insomnia.app.staging.* directories of installs that are no longer running.
Nothing else in ~/Applications is touched. Purge removes owned files, not arbitrary
directory contents. A small shared lock file is retained to keep concurrent
recovery operations coordinated.
Development
swift build
swift test
CI runs Swift tests, a release build with warnings as errors, a bash 3.2 syntax check of the scripts, ShellCheck, and actionlint plus zizmor over the workflows. tmux integration tests need tmux installed; check skip counts rather than assuming missing integration coverage passed. Tests use injected dependencies and temporary fixtures—not live installation or power changes on a contributor's machine.
The app icon is the menu bar's open eye on a charcoal tile: AppIconArtwork draws it from the same vector geometry as the menu bar's closed eye, which opens while a session runs.
After changing the artwork, run ./scripts/generate-app-icon.sh to regenerate
the packaged PNG and ICNS assets and the README's SVG. The script draws them
offline with Xcode's swiftc and iconutil.
Contributing · Security reporting · Release validation · Design notes