macontrol

module
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 22, 2026 License: MIT

README

macontrol

Control your Mac from Telegram — system, media, network, power. Self-hosted · no cloud middleman · named commands only · Apple Silicon · Go.

Apple Silicon Go Release Homebrew

macOS 11 Big Sur macOS 12 Monterey macOS 13 Ventura macOS 14 Sonoma macOS 15 Sequoia macOS 26 Tahoe

CI extra-lint pin-check

CodeQL Gitleaks TruffleHog gosec Trivy govulncheck

OpenSSF Best Practices OpenSSF Scorecard SLSA 3 Cosign signed SBOM License-check Security Policy

codecov Codacy Coverage Codacy Grade Go Report Card

Conventional Commits release-please Commits since latest

Contributor Covenant Renovate enabled Dependabot enabled Last commit GitHub stars

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.

Why

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.

Features

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

Install

brew install amiwrpremium/tap/macontrol
macontrol setup                 # interactive wizard
brew services start macontrol
Manual
curl -fsSL https://raw.githubusercontent.com/amiwrpremium/macontrol/master/scripts/install.sh | sh
macontrol setup
macontrol service install       # writes LaunchAgent plist, launchctl-loads it

Apple 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.

Compatibility

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+.

Documentation

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

Quick reference

Telegram setup in 60 seconds
  1. Create a bot with @BotFather, copy the token.
  2. Get your Telegram user ID from @userinfobot.
  3. macontrol setup — paste both, the wizard does the rest.
  4. Send /start to your bot.

Full walkthrough: docs/getting-started/credentials-telegram.md.

UX model in three lines
  • /menu sends 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.

Architecture

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 shared internal/runner subprocess boundary. A status aggregator 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 from internal/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/.

Permissions

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/.

Development

make lint test            # golangci-lint + go test -race
make build                # cross-compile for darwin/arm64
make run                  # run locally against a dev bot token

Conventional 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.

Continuous integration

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

Required repository secrets

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.

Versioning

Semantic versioning with release-please driving every bump. Conventional Commits on the merge to master decide the next version:

  • feat: … → minor bump
  • fix: … → patch bump
  • feat!: … or a BREAKING CHANGE: footer → major bump
  • chore: …, 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.

Project files

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)

Security

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/.

Disclaimer

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.

Acknowledgments

macontrol stands on the shoulders of:

License

MIT. See LICENSE.

Directories

Path Synopsis
cmd
macontrol command
Command macontrol is the single binary shipped to macOS.
Command macontrol is the single binary shipped to macOS.
internal
capability
Package capability detects what the running macOS supports so the Telegram layer can hide (or politely reject) features that need a newer OS than the host has.
Package capability detects what the running macOS supports so the Telegram layer can hide (or politely reject) features that need a newer OS than the host has.
config
Package config loads runtime secrets from the macOS Keychain.
Package config loads runtime secrets from the macOS Keychain.
domain/apps
Package apps exposes a control surface for the Mac's running user-facing applications: list them, quit them gracefully, force-kill them, or hide their windows.
Package apps exposes a control surface for the Mac's running user-facing applications: list them, quit them gracefully, force-kill them, or hide their windows.
domain/battery
Package battery reads battery percentage, charging state, and long-term health from `pmset -g batt` and `system_profiler SPPowerDataType`.
Package battery reads battery percentage, charging state, and long-term health from `pmset -g batt` and `system_profiler SPPowerDataType`.
domain/bluetooth
Package bluetooth wraps the `blueutil` brew formula to expose Bluetooth radio + paired-device control.
Package bluetooth wraps the `blueutil` brew formula to expose Bluetooth radio + paired-device control.
domain/display
Package display exposes brightness and screensaver control for the built-in display.
Package display exposes brightness and screensaver control for the built-in display.
domain/media
Package media captures still images and recordings of the Mac: screenshots via `screencapture`, screen recordings via the same CLI's record mode, and webcam single-frame photos via the optional `imagesnap` brew formula.
Package media captures still images and recordings of the Mac: screenshots via `screencapture`, screen recordings via the same CLI's record mode, and webcam single-frame photos via the optional `imagesnap` brew formula.
domain/music
Package music exposes player-agnostic playback control + now- playing metadata via the third-party `nowplaying-cli` binary.
Package music exposes player-agnostic playback control + now- playing metadata via the third-party `nowplaying-cli` binary.
domain/notify
Package notify sends desktop notifications and text-to-speech.
Package notify sends desktop notifications and text-to-speech.
domain/power
Package power exposes lock / sleep / restart / shutdown / logout + keep-awake controls.
Package power exposes lock / sleep / restart / shutdown / logout + keep-awake controls.
domain/sound
Package sound exposes macOS audio output controls — volume, mute, and text-to-speech.
Package sound exposes macOS audio output controls — volume, mute, and text-to-speech.
domain/status
Package status aggregates a single one-screen dashboard snapshot by composing the other read-only domain packages.
Package status aggregates a single one-screen dashboard snapshot by composing the other read-only domain packages.
domain/system
Package system exposes read-only macOS inspection: OS version and build, hardware model, chip, memory, CPU load and top processes, thermal pressure, and approximate temperatures.
Package system exposes read-only macOS inspection: OS version and build, hardware model, chip, memory, CPU load and top processes, thermal pressure, and approximate temperatures.
domain/tools
Package tools groups small utility capabilities that don't warrant their own domain package: clipboard read/write, timezone query and set, mounted-volume listing and ejection, and the user-Shortcuts CLI runner.
Package tools groups small utility capabilities that don't warrant their own domain package: clipboard read/write, timezone query and set, mounted-volume listing and ejection, and the user-Shortcuts CLI runner.
domain/wifi
Package wifi controls the Mac's Wi-Fi radio and reads its current state.
Package wifi controls the Mac's Wi-Fi radio and reads its current state.
keychain
Package keychain wraps the macOS `security` CLI to read, write, and delete generic-password Keychain entries owned by macontrol.
Package keychain wraps the macOS `security` CLI to read, write, and delete generic-password Keychain entries owned by macontrol.
runner
Package runner is the macOS-subprocess boundary for the entire bot.
Package runner is the macOS-subprocess boundary for the entire bot.
telegram/bot
Package bot wires the upstream go-telegram client to macontrol's own routers, middleware, and per-domain services.
Package bot wires the upstream go-telegram client to macontrol's own routers, middleware, and per-domain services.
telegram/callbacks
Package callbacks owns the inline-keyboard callback protocol that glues every Telegram tap to the matching domain action.
Package callbacks owns the inline-keyboard callback protocol that glues every Telegram tap to the matching domain action.
telegram/flows
Package flows implements multi-step Telegram conversations.
Package flows implements multi-step Telegram conversations.
telegram/handlers
Package handlers implements the bot's command and callback routers.
Package handlers implements the bot's command and callback routers.
telegram/keyboards
Package keyboards builds Telegram reply and inline keyboards.
Package keyboards builds Telegram reply and inline keyboards.
telegram/musicrefresh
Package musicrefresh owns the per-chat live-refresh goroutines that keep the 🎵 Music dashboard's photo caption in sync with the player's actual position every 5 seconds.
Package musicrefresh owns the per-chat live-refresh goroutines that keep the 🎵 Music dashboard's photo caption in sync with the player's actual position every 5 seconds.
telegram/telegramtest
Package telegramtest provides an httptest-based fake Telegram Bot API server that records every outgoing request.
Package telegramtest provides an httptest-based fake Telegram Bot API server that records every outgoing request.
version
Package version exposes the binary's build-time identity: semver tag (release-please-managed), git short SHA, and build date.
Package version exposes the binary's build-time identity: semver tag (release-please-managed), git short SHA, and build date.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL