Control your Mac from Telegram — system, media, network, power. Self-hosted · no cloud middleman · named commands only · Apple Silicon · Go.
Why · Features · Install · Compatibility · Documentation · Quick reference · Architecture · Permissions · Development · Continuous integration · Repository secrets · Versioning · Project files · Security · Disclaimer · Related projects · Acknowledgments · License
macontrol is a tiny Go daemon that runs on your Mac and exposes a
menu-first Telegram bot for remote control: change volume / brightness,
toggle Wi-Fi / Bluetooth, read battery & system stats, take screenshots,
send desktop notifications, lock / sleep / restart, and more.
For when you're away from your Mac and need to lock it, take a screenshot, peek at battery or Wi-Fi state, or run one of your Shortcuts — without exposing SSH, without a SaaS middleman, without paying anyone.
The daemon lives on your Mac, talks to Telegram via outbound long-poll
only (no inbound port), keeps secrets in the macOS Keychain, and uses
a hard Telegram-user-ID whitelist as the auth boundary. Named commands
only — no /sh escape hatch — so a leaked token alone can't run arbitrary
code on your Mac.
| Category | What you can do |
|---|---|
| 🔊 Sound | Volume ± / set / mute / max |
| 💡 Display | Brightness ± / set, trigger screen saver |
| 🔋 Battery | Percent, charging state, health, cycle count |
| 📶 Wi-Fi | Toggle, info (SSID + BSSID + RSSI + Security + channel), join network, DNS presets, speed test |
| 🔵 Bluetooth | Toggle, list, connect/disconnect paired devices |
| ⚡ Power | Lock, sleep, restart, shutdown, logout, keep-awake |
| 🖥 System | macOS/HW info, thermal pressure, memory + tappable top RAM hogs, CPU + tappable top CPU hogs, Top 10 — every process drills into Kill / Force Kill |
| 🪟 Apps | List running apps, quit / force quit / hide each, "Quit all except…" multi-select |
| 📸 Media | Full/display/window screenshot, screen recording, webcam photo |
| 🎵 Music | Player-agnostic play/pause/next/prev/seek + live progress bar + artwork, with embedded volume controls |
| 🔔 Notify | Desktop notification (terminal-notifier → osascript fallback), text-to-speech |
| 🛠 Tools | Clipboard get/set, timezone pick, time sync, tappable disks (Open in Finder + Eject for removables), run any Shortcut |
brew install amiwrpremium/tap/macontrol
macontrol setup # interactive wizard
brew services start macontrolcurl -fsSL https://raw.githubusercontent.com/amiwrpremium/macontrol/master/scripts/install.sh | sh
macontrol setup
macontrol service install # writes LaunchAgent plist, launchctl-loads itApple Silicon, macOS 11 (Big Sur) or newer. Intel is not supported.
For build-from-source, install-script internals, and uninstall steps, see docs/getting-started/installation.md.
| Layer | Requirement |
|---|---|
| Architecture | Apple Silicon (arm64) only — no Intel, no Rosetta |
| macOS | 11 (Big Sur) minimum; features unlock per-version (see version-gates) |
| Go (build-from-source) | as declared in go.mod |
| Telegram | bot token + your numeric user ID (both via macontrol setup) |
| Optional brew formulae | brightness, blueutil, smctemp, imagesnap, terminal-notifier, nowplaying-cli — installed automatically by the tap; missing ones degrade specific features without breaking the daemon |
The daemon runs a capability check at startup (visible via macontrol doctor)
and only renders buttons for features the host's macOS version actually
supports. A bot running on macOS 11 silently hides buttons that need macOS 14+.
The docs/ directory is the full reference. Pick a group:
| Group | What's there |
|---|---|
| Getting started | Install → credentials → quickstart → first message |
| Usage | UX model, slash commands, every button in every category |
| Configuration | Runtime config (CLI flags + Keychain), file paths, whitelist management |
| Permissions | TCC grants and the narrow sudoers entry |
| Operations | Running, logs, doctor, upgrades |
| Architecture | Overview, project layout, design decisions, testing |
| Reference | CLI flags, callback protocol, macOS CLI mapping, version gates |
| Security | Bot token hygiene, threat model, vulnerability reporting |
| Troubleshooting | Common issues, permission errors, Telegram errors |
| Development | Contributing, conventional commits, adding a capability, releasing |
| FAQ | Quick answers grouped by topic |
| Changelog | What changed in each release |
- Create a bot with @BotFather, copy the token.
- Get your Telegram user ID from @userinfobot.
macontrol setup— paste both, the wizard does the rest.- Send
/startto your bot.
Full walkthrough: docs/getting-started/credentials-telegram.md.
/menusends an inline keyboard with one button per category.- Tapping a category edits the message into that category's dashboard, which itself edits in place as you tap (
+5,MUTE,🔄 Refresh, …). - Free-text input (set exact volume, join wifi, …) drops into a 5-min flow.
Deep explanation: docs/usage/ux-model.md.
One Go binary running as a LaunchAgent. Three layers:
- Domain services (
internal/domain/<area>/) — 13 services covering apps, music, sound, display, battery, wifi, bluetooth, power, system, media, notify, tools, each shelling out to macOS CLIs via the sharedinternal/runnersubprocess boundary. Astatusaggregator combines their reports for the dashboard view. - Telegram layer (
internal/telegram/{bot,handlers,keyboards,callbacks,flows,musicrefresh}/) — dispatcher routes messages and inline-keyboard callbacks to per-domain handlers; each handler renders a keyboard frominternal/telegram/keyboards/<area>.go; multi-step flows (set exact volume, join Wi-Fi, …) run through a TTL-keyed flow registry. - CLI + lifecycle (
cmd/macontrol/) — subcommand dispatcher, daemon lifecycle, setup wizard, doctor self-check, service install (LaunchAgent), Keychain-backed config.
Callback-data protocol: <namespace>:<action>[:<arg>], packed into Telegram's 64-byte limit; overflow keys use a ShortMap side table indexed by 10-char base32 IDs.
Full text + diagrams: docs/architecture/.
macontrol asks for three things at the system level:
| What | Why | How |
|---|---|---|
| TCC grants | Screen Recording (screenshots + screen recording), Camera (imagesnap for webcam photos), Accessibility (some media controls) |
macOS prompts on first use; can be pre-granted in System Settings → Privacy & Security |
| Sudoers fragment | NOPASSWD for the narrow set of binaries the daemon shells out to as root: pmset, shutdown, wdutil, powermetrics, systemsetup |
macontrol setup writes /etc/sudoers.d/macontrol, or copy from sudoers.d/macontrol.sample |
| Keychain entries | Bot token + Telegram user-ID whitelist; the daemon reads them at startup. ACL is bound to the binary path so a copy of the binary in another location cannot read the entries. | macontrol token set + macontrol whitelist add <id> (also via the setup wizard) |
Deep dive: docs/permissions/.
make lint test # golangci-lint + go test -race
make build # cross-compile for darwin/arm64
make run # run locally against a dev bot tokenConventional Commits required for PR titles. Releases are cut by release-please — merging the version PR triggers GoReleaser, which builds the tarball and updates the Homebrew tap automatically.
Full guide: docs/development/. By contributing you agree to the Code of Conduct.
| Workflow | Triggers | Gates |
|---|---|---|
ci.yml |
push, PR | lint (golangci-lint v2), test (-race, matrix: ubuntu-latest + macos-14), build (darwin/arm64), govulncheck, short fuzz pass |
codeql.yml |
push, PR, weekly | CodeQL SAST → Security tab |
gosec.yml |
push, PR | gosec SARIF → Security tab |
trivy.yml |
push, PR, daily | filesystem + secret + config (IaC) scans → Security tab |
gitleaks.yml |
push, PR, weekly | regex-based secret scan over full history |
trufflehog.yml |
push, PR, weekly | entropy + active-verifier secret scan |
dependency-review.yml |
PR | GitHub Dependency Review (high-severity vulns block, AGPL/GPL deny) |
license-check.yml |
push, PR | go-licenses check against permissive-license allow-list |
extra-lint.yml |
push, PR | markdownlint / yamllint / actionlint / editorconfig-checker / typos |
pin-check.yml |
PR on workflow files | every uses: must be SHA-pinned |
pr-title.yml |
PR | Conventional Commits + 37-scope allow-list |
scorecards.yml |
push, weekly | OpenSSF Scorecard → Security tab |
release-please.yml |
push on master | open/maintain the release PR; cut tags |
release.yml |
v* tag push |
goreleaser → binaries + SBOMs + cosign sigs + brew tap update; SLSA L3 provenance |
labeler.yml |
PR | apply area:* labels by changed file paths |
auto-assign.yml |
PR | reviewer + assignee on human-authored PRs |
stale.yml |
weekly | mark + close inactive issues + PRs |
| Secret | Scope | Used by | What it unblocks |
|---|---|---|---|
RELEASE_PLEASE_PAT |
Actions | release-please.yml |
Fine-grained PAT (contents:write + pull-requests:write). Required because tags created with GITHUB_TOKEN don't trigger downstream release.yml. |
HOMEBREW_TAP_TOKEN |
Actions | release.yml |
Fine-grained PAT on amiwrpremium/homebrew-tap (contents:write) so goreleaser can update the formula in a separate repo. |
CODECOV_TOKEN |
Actions | ci.yml |
Codecov coverage upload from the test job. |
CODACY_PROJECT_TOKEN |
Actions and Dependabot | ci.yml + Codacy reporter |
Codacy coverage upload. Needs the matching Dependabot-scoped copy so Dependabot PRs also report coverage. |
Created in Settings → Secrets and variables. Rotate yearly.
Semantic versioning with release-please driving every bump. Conventional Commits on the merge to master decide the next version:
feat: …→ minor bumpfix: …→ patch bumpfeat!: …or aBREAKING CHANGE:footer → major bumpchore: …,docs: …,test: …, etc. → no bump
Public surface stable as of v1.0.0: any breaking change to the Telegram command set, the inline-keyboard callback protocol, the CLI subcommands, the Keychain entry names, the sudoers fragment, or the LaunchAgent contract bumps the major version.
Source-controlled semver lives in internal/version/version.go (annotated // x-release-please-version); release-please rewrites that literal on every release cut. Commit + build date are stamped at link time by goreleaser ldflags.
| Path | What |
|---|---|
cmd/macontrol/ |
CLI entrypoint, daemon lifecycle, setup / service / doctor / token / whitelist subcommands |
internal/domain/ |
13 services per macOS surface + status aggregator |
internal/telegram/ |
Bot dispatcher, callback-data protocol, per-domain handlers + keyboards, multi-step flow registry, music live-refresh |
internal/runner/ |
Subprocess execution boundary — every macOS interaction shells out through here |
internal/capability/ |
macOS feature detection |
internal/config/ |
Keychain-backed config + CLI flag parsing |
internal/keychain/ |
macOS Keychain wrapper |
internal/version/ |
Release-please-managed semver const + goreleaser-stamped commit/date |
launchd/ |
LaunchAgent plist template |
sudoers.d/ |
Sudoers fragment template |
scripts/ |
install.sh for the non-Homebrew path |
docs/ |
Full reference documentation |
.github/ |
Workflows, issue/PR templates, CODEOWNERS, dependency-manager configs, label palette, rulesets, settings.yml |
Makefile |
Local dev runner: build / test / lint / tools / hooks / release-dry |
lefthook.yml |
Opt-in git hooks (pre-commit / commit-msg / pre-push) |
.golangci.yml |
Linter ruleset |
.goreleaser.yaml |
Release pipeline (binary + SBOM + cosign sig + brew formula update) |
release-please-config.json |
Release-please bump rules + extra-file pointer |
renovate.json |
Renovate config (primary dep manager) |
Never share your bot token. macontrol enforces a hard user-ID whitelist; non-whitelisted updates are dropped silently.
Report vulnerabilities privately via GitHub Security Advisories — see SECURITY.md and docs/security/.
macontrol is provided as is, without warranty of any kind, express or
implied — see the MIT License for the full text.
By installing and running this software you acknowledge and accept that:
- It controls your Mac. The bot can lock, restart, shut down, or log out your session; take screenshots and webcam photos; record your screen; change DNS and Wi-Fi settings; and run any Shortcut you have configured. Misuse, misconfiguration, or compromise of the bot token or your Telegram account can lead to data loss, privacy exposure, or other harm to you or your machine.
- You are responsible for the bot token and the whitelist. Anyone with the token can act as your bot; anyone whose Telegram user ID is on the whitelist has the same control over your Mac that you do.
- You are responsible for third-party trust anchors. macontrol shells
out to macOS CLIs (
pmset,networksetup,security, …) and optional Homebrew formulae (brightness,blueutil,smctemp,imagesnap,terminal-notifier). Telegram, Apple, and Homebrew sit outside the author's control. - The author (
@amiwrpremium) is not liable for damages, data loss, privacy incidents, unauthorized access, or any other harm resulting from the use, misuse, or failure of this software — whether direct, indirect, incidental, or consequential. - No support guarantees. This is a personal project. Issues and pull requests are welcome, but there is no SLA, no paid support, and no commitment to fix any specific bug.
- Use at your own risk.
- shellboto — the Linux-VPS sibling. Where macontrol exposes only named commands on macOS, shellboto gives whitelisted users a live, pty-backed bash shell on a Linux server with SHA-256 hash-chained audit logs and per-user RBAC. Different scope, different security model — same author, same Go + Telegram-bot patterns.
macontrol stands on the shoulders of:
- go-telegram/bot — the Go Telegram-Bot client
- GoReleaser + release-please — the release automation
- lumberjack — log rotation
- Homebrew formulae bundled as runtime deps: brightness, blueutil, smctemp (upstream by @narugit, formula mirrored in our tap — see FAQ), imagesnap, terminal-notifier, nowplaying-cli (upstream by @kirtan-shah) — wraps Apple's private MediaRemote.framework
- Apple's macOS CLIs that do all the actual work:
pmset,osascript,networksetup,security,screencapture,wdutil,pbpaste/pbcopy,say
MIT. See LICENSE.
