Nix
The second way to install Arin on a Mac, after the Homebrew tap. It needs Nix with flakes enabled, and nothing else: no Rust toolchain, no Xcode.
nix run github:anistark/arin -- -d
That builds Arin and starts the daemon. Bare nix run github:anistark/arin prints help,
because bare arin prints help everywhere else and a command that reads like a question
should not leave a daemon running behind it.
To keep it:
nix profile install github:anistark/arin
macOS 14 or newer, on Apple silicon or Intel. There is no Linux package, and that is not an oversight: the Linux renderer is v2, so the daemon would speak the whole protocol and draw nothing.
What gets installed
Arin.app, not a bare binary. macOS keys the Screen Recording grant to a bundle, the menu
bar item and the absent Dock icon are both properties of Info.plist, and the app
recognises itself when Finder opens it by the identifier LaunchServices hands it. A package
that installed bin/arin alone would build the same code and be a different product.
result/
Applications/Arin.app the bundle, with launch-agent.sh in its Resources
bin/arin a symlink into the bundle, so PATH and the app are one binary
Installed into a profile or through environment.systemPackages, the app is linked into
/Applications/Nix Apps/, which is where Spotlight will find it.
Starting it at login
The flake ships a nix-darwin module.
{
inputs.arin.url = "github:anistark/arin";
# in your darwinConfiguration
modules = [
arin.darwinModules.default
{
services.arin = {
enable = true;
resolver = "local";
};
}
];
}
That writes ~/Library/LaunchAgents/com.anistark.arin.plist and starts the daemon at
login, from the binary inside the bundle. It is the same launch agent arin service enable
installs, under the same label, so the two collide rather than running two daemons against
one socket. Pick one.
Both sides now say so rather than racing. nix-darwin refuses to overwrite a plist it did
not write and tells you to move it out of the way, and arin service recognises a
nix-managed agent by the store path in it and refuses to touch it. To move from the module
to the command, unset services.arin.enable, rebuild, and run arin service enable.
nix-darwin needs system.primaryUser set to own a user agent.
Options
Every option below maps to a flag on arin daemon, and leaving one unset leaves the
daemon's own default in place. arin daemon --help is the fuller explanation of what each
one does.
| Option | Type | Default | What it does |
|---|---|---|---|
services.arin.enable |
bool | false |
Start the daemon at login |
services.arin.package |
package | this flake's | Has to be one containing Arin.app |
services.arin.socket |
path or null | null |
Where the daemon listens |
services.arin.headless |
bool | false |
Run the protocol and draw nothing |
services.arin.resolver |
string or null | null |
Ground natural language targets with this resolver |
services.arin.groundingConsent |
ask, always, never, or null |
null |
Whether a client may make Arin look at the screen |
services.arin.color |
string or null | null |
Preferred mark colour, as #RRGGBB |
services.arin.palette |
list of string | [ ] |
The full set of colours to choose between |
services.arin.adaptiveColor |
bool | true |
Look at the screen to pick a visible colour |
services.arin.checkUpdates |
bool | false |
Ask GitHub once a day for a newer version |
services.arin.extraFlags |
list of string | [ ] |
Anything the module does not name yet |
services.arin.logFile |
string or null | /tmp/arin.log |
Where the agent's output goes |
logFile has to be absolute, in a directory that already exists. launchd does not expand
~ and will not create a parent, and a Nix module cannot know your home directory while it
is being evaluated, so the default is somewhere that always exists rather than somewhere
good.
Naming a resolver is consent to the daemon reading your screen when a client asks it to.
local sends nothing off the machine. claude uploads a screenshot per query. Arin asks
before the first one either way, unless you set groundingConsent = "always", which is
what an unattended daemon wants and is worth writing down deliberately rather than
arriving at.
The overlay
For pulling Arin into your own package set:
nixpkgs.overlays = [ arin.overlays.default ]; # gives you pkgs.arin
Permissions, and what an update costs
Arin asks for Screen Recording on first run. It is the only permission it ever asks for, and it needs it for two things: noticing when content moves under a mark, and picking a colour that can be seen against whatever is underneath.
Today, every update asks again. Nothing is signed yet, and macOS identifies unsigned
code by its hash and its path, both of which change on every Nix build. This is not
peculiar to Nix, brew upgrade has the same problem, but a store path makes it visible.
Signing lands in 0.7, after which the grant follows the certificate and the bundle
identifier rather than the path, and updating stops costing anything.
If the daemon keeps asking after you have granted it, two builds are competing for one row in System Settings:
tccutil reset ScreenCapture com.anistark.arin
Then start whichever one you meant to run.
Building on it
nix develop # the toolchain and the tools the justfile reaches for
nix flake check # builds the package
nix fmt # nixfmt
Building the package does not run the test suite. CI runs it on every pull request against the same code, and making every person who installs Arin run it again is most of the difference between a slow install and a quick one.
If you would rather pay the minutes than take CI's word for it, there is a second package that runs the whole workspace suite inside the build:
nix run github:anistark/arin#arin-tested -- -d
nix build github:anistark/arin#arin-tested
It is the same Arin. doCheck is part of a derivation, so it compiles from scratch rather
than reusing the untested build, and it is worth it mainly when you are installing from a
branch or a commit CI has never seen. nix flake check builds both.
The dev shell is offered rather than required. Arin is developed here with rustup and the
system Xcode, and what the shell has to stay is one in which just ci passes.
Intel
x86_64-darwin is exposed and evaluated in CI, and it is not built there: GitHub's Intel
runners are being retired and nixpkgs 26.05 is the last release that supports the platform
at all. It should work. Nobody has watched it.