やめて — stop it.
Slap detection for Apple Silicon MacBooks. Hit the laptop, it makes a noise.
The interesting part is that it works at all: the sensor it reads is an undocumented Bosch IMU behind the Sensor Processing Unit, invisible to CoreMotion, reachable only as vendor-usage-page HID devices. Telling a slap apart from someone thumping the desk turns out to need the gyroscope, which is not where you would first look.
An Apple Silicon laptop, macOS 13 or later. The sensor exists on M2 and later, plus
M1 Pro and M1 Max. It is absent from the M1 MacBook Pro (2020), the M1 Air, every desktop
Mac, and every Intel Mac — yamete probe says so plainly rather than hanging.
The first launch asks for Input Monitoring. macOS gates all HID access behind it, including a sensor that is not an input device in any useful sense.
crates/yamete-sensor |
IOKit HID access to the accelerometer and gyroscope, ~805 Hz |
crates/yamete-dsp |
The detector. No platform or I/O dependencies, so it can be replayed against recordings in a test |
crates/yamete-proto |
Wire types for the control socket |
crates/yamete |
The daemon, its actions, and the tuning tools |
app/ |
Yamete, the Tauri menu bar app |
fixtures/ |
40 annotated slaps and 150 s of things that must stay silent |
The daemon does everything real: reads the sensor, runs the detector, fires actions. The app is a controller that attaches over a unix socket.
The app bundles the daemon as a sidecar and owns its lifetime — opening Yamete starts
detection, quitting it stops detection. The daemon is spawned with a stdin pipe and
--exit-with-parent, so the pipe closing kills it however the app dies, including a force
quit. If a daemon is already running (a LaunchAgent, or one started from a terminal) the
app attaches to it and leaves it alone on exit.
| socket | ~/Library/Application Support/com.chipcolate.yamete/yamete.sock |
| config | …/com.chipcolate.yamete/config.json |
| logs | ~/Library/Logs/yamete/yamete.log (previous run kept as .log.1) |
The protocol is newline-delimited JSON, so the daemon can be driven by hand:
nc -U ~/Library/Application\ Support/com.chipcolate.yamete/yamete.sock
{"cmd":"get_status"}
{"cmd":"subscribe","slaps":true}./build-app.shProduces a signed .app and .dmg, then reports whether the signature is valid, whether
a notarisation ticket is stapled, and whether Gatekeeper would accept it. It deliberately
does not install or launch anything — install by opening the DMG and dragging across.
The script stages the daemon into the app bundle before building it. That order matters:
the app spawns the bundled yamete, so building against a stale copy produces a bundle
whose daemon rejects arguments the app passes it, and fails silently because the child
exits before it can log anything.
cargo test --workspace # unit tests plus a replay of the whole fixture corpus
cargo clippy --workspace --all-targets -- -D warnings
cd app && bun run tauri dev # the app against a live frontend, for UI workCI runs those on every PR against main, plus an unsigned bundle build — bundling is the
only thing that exercises tauri.conf.json, the capability set, the icon list and the
sidecar staging, none of which the compiler sees.
Tagging v* builds, signs, notarises and publishes a GitHub Release with the DMG and its
checksum. That needs six repository secrets: APPLE_CERTIFICATE (base64 of the Developer
ID .p12), APPLE_CERTIFICATE_PASSWORD, APPLE_SIGNING_IDENTITY, APPLE_API_ISSUER,
APPLE_API_KEY and APPLE_API_KEY_CONTENT (the .p8 file's contents). Without them the
release still builds, just unsigned — and so it will not open on anyone else's Mac. The
tag has to match the version in both Cargo.toml and tauri.conf.json.
A signing identity is picked from the keychain automatically, preferring a
Developer ID Application certificate over Apple Development. Override with
APPLE_SIGNING_IDENTITY.
Signing matters beyond Gatekeeper: the Input Monitoring grant that lets the daemon read the sensor is tied to the signing identity, and an ad-hoc signature changes every build — so macOS can re-prompt for permission after each rebuild.
To produce something that opens on someone else's Mac you need a Developer ID certificate and notarisation credentials — a Developer ID signature alone is not enough, since Gatekeeper has also required a notarisation ticket from macOS 10.15 onwards. Create the certificate at developer.apple.com → Certificates (Developer ID Application, G2 Sub-CA), download it, and double-click to install.
Then either an App Store Connect API key:
export APPLE_API_ISSUER=... # the issuer UUID
export APPLE_API_KEY=... # the key ID
export APPLE_API_KEY_PATH=~/.appstoreconnect/private_keys/AuthKey_XXXXXXXXXX.p8or an Apple ID with an app-specific password:
export APPLE_ID=you@example.com
export APPLE_PASSWORD=abcd-efgh-ijkl-mnop
export APPLE_TEAM_ID=XXXXXXXXXXThe build then notarises and staples, and the verification step should report
Gatekeeper: accepted. Both the app and the DMG are submitted, so that is two round trips
to Apple; queue times vary from minutes to considerably longer.
yamete runs happily without the app, which is how the detector is developed.
yamete probe # is the sensor there, at what rate, decoding correctly
yamete watch --scores # live detections, with the five detector scores
yamete status # what the running daemon thinks
yamete listen # stream detections as they happen
yamete install --copy # run it permanently as a LaunchAgent, independent of the appA LaunchAgent is only needed if you want detection without the app running. It is a user agent rather than a system daemon, which is not merely simpler but necessary: Input Monitoring is a per-user GUI consent, and a root daemon has no login session to prompt in.
Thresholds are derived from recordings, not guessed. The obvious-looking values do not survive contact with the hardware: a 0.005 g micro-shock floor sits around the 95th percentile of an idle laptop, so a detector built on it fires on nothing but noise.
yamete record-suite # record the corpus, with a metronome
yamete analyze fixtures/idle.fixture.gz # what the detectors read when quiet
yamete analyze fixtures/slap-*.gz --at-detections # and at a real slap
yamete sweep fixtures/*.gz --knob gyro-ratio # score a threshold against everything
yamete replay fixtures/*.gz -v # what the current settings would docargo test replays the whole corpus and holds the detector to a measured envelope:
everyday activity silent, false positives within budget, recall above target. The suite
skips rather than fails when fixtures/ is empty, so a fresh clone still passes.
--at-detections is the one worth knowing about. Percentiles over a whole recording are
dominated by the quiet 98 % of it and say nothing about whether a threshold is reachable;
sampling the detectors at the moment one fires is what tells you whether a statistic
contributes at all.
Apache License 2.0. See LICENSE and NOTICE.
Copyright 2026 CHIPCOLATE SRL.