Perch
Building modsScopes

Scopes

Every capability a mod can ask for, what each one installs, and how users see them.

5 min readUpdated Sep 7, 2026

Scopes are strings in the scopes array of mod.json. Each one installs a set of perch.* functions and signals. Nothing else is granted: a mod without media has no perch.media table at all, so perch.media.read() and perch.media.on_change:connect(...) both raise attempt to index a nil value.

{ "scopes": ["media", "media.control", "net.http:api.github.com"] }

The array is shown verbatim on the Workshop page (with a plain-language line per scope) and on the mod’s card in Settings, Installed mods. Subscribing is the consent. Ask for what you use, nothing more: a long scope list is the first thing a cautious user reads.

The Installed mods page listing packages with their scope chips

The table

ScopeInstallsSignalsPage
mediaperch.media.read(), perch.audio.spectrum()perch.media.on_changeMedia, Audio
media.controlperch.media.play_pause(), .next(), .previous(), .seek(us), .refresh(), .open_player()Media
discordperch.discord.read()perch.discord.on_call, .on_notification, .on_ring, .on_statusDiscord
discord.controlperch.discord.toggle_mute(), .toggle_deafen(), .hangup(), .accept_ring(), .decline_ring(), .connect(), .disconnect(), .configure(tbl)Discord
weatherperch.weather.read(), .configure(tbl)perch.weather.on_changeWeather
system.statsperch.system.performance.read()System
system.transientsperch.audio.volume.on_change, perch.system.display.on_brightness_change, .on_night_light_change, perch.system.connectivity.on_change, perch.system.timers.on_set, .on_tick, .on_done, .on_cancelSystem
volume.controlperch.audio.volume.up(), .down(), .set(pct)Audio
audio.playperch.audio.play(file)Audio
audio.mixerperch.audio.sessions(), .set_session_volume(pid, level), .set_session_muted(pid, muted)Audio
audio.microphoneperch.audio.microphone.read()Audio
system.powerperch.system.power.read(), perch.system.peripherals.list()perch.system.power.on_changeSystem
system.downloadsperch.system.downloads.on_progressSystem
system.notificationsperch.system.notifications.configure(tbl)perch.system.notifications.on_notificationSystem
system.clipboardperch.system.clipboard.read()perch.system.clipboard.on_changeSystem
input.hotkeysperch.input.hotkeys.register(combination) returning a handle with :release()the handle’s on_pressInput
appsperch.apps.read(), .launch(entry), .configure(tbl), .focused()perch.apps.on_focus_changeApps
openperch.open.url(url), .steam(appid), .app(ref)Open
steamperch.steam.installed(), .running(), and the callback lookups .player_count(appid, cb), .app_info(appid, cb), .player_history(appid, cb), .artwork(appid, cb), .store_details(appid, cb), .news(appid, count?, cb), .search(term, cb)perch.steam.on_game_change, plus a landing signal per lookup: .on_player_count, .on_player_history, .on_app_info, .on_artwork, .on_store_details, .on_news, .on_searchSteam
mods.messagingperch.mods.channel(name) returning a channel with :publish(payload)the channel’s on_messageChannels
net.http:<host>perch.net.http.get(url, opts?, cb), perch.net.http.post(url, body, opts?, cb) for that host, answering through cb(res, err); repeat the scope per hostnone (the callback is the answer)Net
net.websocket:<host>perch.net.websocket.connect(url, opts) for that host, wss:// only, 4 sockets per modthe handle’s on_message, on_closeNet

Always granted

No scope needed for the pure globals (ui, field, settings, widget, timer, key, surface, urgency), the g canvas a ui.draw painter receives, or the always-on part of perch: perch.log, perch.settings.declare, perch.voice.on_intent, perch.storage.*, perch.json.*, perch.time.*, perch.hash.*, perch.base64.*, perch.uuid.generate, perch.locale.read, perch.text.*, and the lifecycle signals perch.on_start, perch.on_stop, perch.on_configure. None of them touch anything outside the mod, so there is nothing to disclose. (perch.voice.on_intent hears only the intents the router already recognised, never the audio or the transcript.)

Dotted narrowing

discord reads; discord.control acts. media reads; media.control acts. A widget that only displays what is playing asks for media alone, and the Workshop page says so.

audio.play is narrower than it sounds: it plays a WAV the mod shipped in its own package, so the scope grants no filesystem access and no path out of the package. audio.mixer is the wide one on that page, because reading another application’s volume also means moving it.

mods.messaging is narrow in a different way: both sides need it. A mod that does not declare it neither publishes nor hears anything, so two mods only talk when their users can see that both asked to.

Host pinning

net.http:api.github.com allows https://api.github.com/... and nothing else: no other host, no subdomains, no http. net.websocket:<host> pins a socket host the same way, and wss:// is the only scheme. Each host is its own scope entry, each one listed on the Workshop page. Before pinning a host, check whether Perch already provides the data (Steam, Weather).

The loud three

Three scopes see things a user would want to know about before installing, and Perch words all three plainly on the Workshop page and in Settings.

  • system.clipboard reads everything the user copies, including passwords, card numbers and private messages. Ask for it only when copying is the point of the mod.
  • system.notifications mirrors the notifications other applications post, message bodies included, and only after the mod calls configure({ mirror = true }).
  • audio.microphone tells a mod whether the microphone is capturing and which applications are using it. It is read only by design: Perch’s own microphone-live indicator is a trust surface, so no mod can mute the microphone or fake its state.

input.hotkeys is not in that list because it sees no typing: a mod hears only the combinations it claimed, and every claim is listed in Settings so the user can see what a mod took.

What is not available

There is no filesystem scope, no process scope, no raw input. perch.open is the only way out of the sandbox and each of its three paths is validated. Unknown scope strings are ignored rather than rejected, so a typo silently grants nothing and the library it should have installed is simply missing; Perch Studio’s validator flags scopes it does not recognize.

esc
Type to search
navigate open