ops-voice — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ops-voice (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Voice / phone / video interface. All API calls via curl — no SDK dependencies. Native macOS handlers (Phone.app, FaceTime, Zoom) require no credentials; programmatic channels (Twilio, Bland, ElevenLabs, Groq, Zoom schedule) resolve credentials via:
Credential resolution order: env vars → ops_cred_get (lib/credential-store.sh / keychain) → preferences.json → Doppler CLI (doppler secrets get <KEY> --plain) → password manager.
All sub-commands have a thin bash wrapper at bin/ops-voice — prefer it over inline curl when scripting.
Outbound comms guardrail (Rule 6): twilio-call, twilio-sms, and bland-call are 1:1 outbound channels and MUST follow the per-message approval gate — stage final draft, show full target+body, wait for explicit approval (AskUserQuestion or single-word chat approval), send one, then stage the next. Never batch.
Parse $ARGUMENTS for the command keyword, then execute. Native handlers exit fast; API calls report status and (where relevant) a poll command.
phone <number> — Native Phone.app via ContinuityRoutes through the linked iPhone. Requires macOS + iPhone signed in to the same iCloud account with Calls on Other Devices enabled. No credentials.
bin/ops-voice phone "+1234567890" --json
# {"ok":true,"channel":"phone","detail":"dialing +1234567890 via Phone.app (Continuity)"}Under the hood: open "tel:<E.164>".
facetime <number-or-email> [--audio] — FaceTime video/audioDefaults to video. Pass --audio for FaceTime Audio (free, Apple↔Apple). Accepts phone numbers or Apple-ID emails.
bin/ops-voice facetime [email protected] # video
bin/ops-voice facetime "+1234567890" --audio --json # audioUnder the hood: open "facetime://<handle>" or open "facetime-audio://<handle>".
zoom start|join|schedule — Zoom meetings# Open zoom.us app and start a new instant meeting
bin/ops-voice zoom start
# Join an existing meeting
bin/ops-voice zoom join 1234567890 --pwd <password>
# Schedule a meeting via Zoom REST API (requires ZOOM_API_TOKEN — Server-to-Server OAuth access token)
bin/ops-voice zoom schedule "<topic>" --start "2026-05-22T15:00:00Z" --duration 30 --json
# {"ok":true,"channel":"zoom","detail":"scheduled meeting 12345 — https://us05web.zoom.us/j/..."}Native start/join use zoommtg:// URL scheme. Schedule requires ZOOM_API_TOKEN resolved via the order above. Generate a Server-to-Server OAuth app in your Zoom Marketplace, then exchange for an access token.
meet start|join — Google Meet# Open a brand-new instant meeting (https://meet.new)
bin/ops-voice meet start
# Join by meeting code (xxx-yyyy-zzz) or full URL
bin/ops-voice meet join abc-defg-hij
bin/ops-voice meet join https://meet.google.com/abc-defg-hij --jsonmeet start opens https://meet.new in the default browser (creates a fresh meeting and lands you in it). meet join accepts a 3-4-3 Google Meet code, a bare code, or a full URL — all are normalized to https://meet.google.com/<code>. No credentials needed; the user must be signed into Google in the browser.
whatsapp-call <number> [--video] — WhatsApp Desktop voice/video call# Voice call (default)
bin/ops-voice whatsapp-call "+1234567890"
# Video call
bin/ops-voice whatsapp-call "+1234567890" --video --jsonOpens WhatsApp Desktop via the whatsapp://call?phone=<digits> or whatsapp://video?phone=<digits> URL scheme. Requires WhatsApp Desktop installed and signed in. The leading + is stripped — WhatsApp expects digits only after phone=. No credentials needed; the WhatsApp session lives in the desktop app.
join [--at now|next|HH:MM] [--window MIN] [--dry-run] — Smart calendar-driven meeting joinerAuto-joins the meeting that's happening now (within ±window minutes, default 10) or the next future meeting if nothing is current. Reads gog calendar events --all --today -j --sort start, extracts a conference URL from hangoutLink → conferenceData.entryPoints[] → location → description scan (supports Zoom, Google Meet, Microsoft Teams, Webex), applies the smart AV policy below, and hands off to the native opener (which honors Rule 7 on SSH/mobile).
bin/ops-voice join # join current/next meeting
bin/ops-voice join --at next # skip current, go to next future
bin/ops-voice join --window 5 # only consider events within ±5min of now
bin/ops-voice join --dry-run # show what would happen — no launch
bin/ops-voice join --dry-run --jsonAV policy (smart heuristic):
| Attendees | Camera | Microphone |
|---|---|---|
| 1–2 | ON | ON |
| 3–9 | ON | MUTED |
| 10+ | OFF | MUTED |
Per-event overrides — tag the event description (case-insensitive):
[cam:on] [mic:off] → force camera on, mic muted
[cam:off] → force camera off (keep heuristic mic)
[mic:muted] → force mic mutedMic source — lid state:
ioreg AppleClamshellState → Yes=closed (external mic), No=open (MacBook mic)./proc/acpi/button/lid/*/state → "open"/"closed".default; the meeting app uses whatever the system has selected.Note: the script reports the policy and launches Camera Hub when present, but it does not programmatically flip Zoom/FaceTime/Meet in-app device settings — those apps remember the last-selected device, so flipping it once per app is permanent. (A future patch could AppleScript Zoom's preferences pane.)
Elgato Virtual Camera: if Elgato Camera Hub is installed (any OS), it's launched before the meeting opens so the virtual cam is registered. Detection paths:
/Applications/Elgato Camera Hub.app, ~/Applications/Elgato Camera Hub.app, /Applications/Camera Hub.appelgato-camera-hub on PATH, or AppImage at ~/Applications/Elgato*CameraHub*.AppImage${PROGRAMFILES}/Elgato/CameraHub/CameraHub.exeZoom URL rewriting: when the picked event has a https://zoom.us/j/<ID>?pwd=<PWD> link, it's converted to zoommtg://zoom.us/join?confno=<ID>&pwd=<PWD> so the desktop app opens directly (no browser prompt). If cam=off from the policy, &zc=0 is appended.
Dry-run output (text mode):
dry-run: would join "Weekly Sync" (meet, 3 attendees)
url=https://meet.google.com/abc-defg-hij
cam=on mic=muted
lid=closed mic_source=external
elgato_hub=/Applications/Elgato Camera Hub.apptwilio-call <to> <from> --twiml <URL> — Programmatic outbound voiceReal telco call (per-minute cost). Requires TWILIO_ACCOUNT_SID + TWILIO_AUTH_TOKEN. The --twiml URL must return TwiML XML describing call behavior — e.g. https://demo.twilio.com/docs/voice.xml or a custom Function/Studio flow.
bin/ops-voice twilio-call "+1234567890" "+15551234567" \
--twiml "https://demo.twilio.com/docs/voice.xml" --jsontwilio-sms <to> <from> "<body>" — Programmatic outbound SMSbin/ops-voice twilio-sms "+1234567890" "+15551234567" "<body>" --jsonFor inbound SMS / WhatsApp routing, point your Twilio webhook at the ops daemon (out of scope for v1).
bland-call <number> "<prompt>" — Bland AI agent phone callAI agent calls the number and follows the natural-language prompt. Recordings + transcripts available via the poll URL printed on success.
bin/ops-voice bland-call "+1234567890" "<task prompt>" --jsonPoll with: curl -H "authorization: $BLAND_AI_API_KEY" https://api.bland.ai/v1/calls/<call_id>.
tts [text] [--voice voice_id] [--out file.mp3] — ElevenLabs text-to-speechRequires: ELEVENLABS_API_KEY (env / keychain / Doppler).
EL_KEY="${ELEVENLABS_API_KEY:-$(doppler secrets get ELEVENLABS_API_KEY --plain 2>/dev/null || true)}"
VOICE_ID="${ELEVENLABS_VOICE_ID:-21m00Tcm4TlvDq8ikWAM}" # Rachel
TEXT="<from $ARGUMENTS>"
OUT_FILE="${OUT_FILE:-/tmp/ops-tts-$(date +%s).mp3}"
curl -s -X POST "https://api.elevenlabs.io/v1/text-to-speech/${VOICE_ID}" \
-H "xi-api-key: $EL_KEY" \
-H "Content-Type: application/json" \
-d "{
\"text\": \"$TEXT\",
\"model_id\": \"eleven_monolingual_v1\",
\"voice_settings\": {\"stability\": 0.5, \"similarity_boost\": 0.75}
}" \
--output "$OUT_FILE"
command -v afplay >/dev/null && afplay "$OUT_FILE" &
echo "Audio saved to: $OUT_FILE"transcribe [file_path] — Groq Whisper transcriptionRequires: GROQ_API_KEY (env / keychain / Doppler).
GROQ_KEY="${GROQ_API_KEY:-$(doppler secrets get GROQ_API_KEY --plain 2>/dev/null || true)}"
AUDIO_FILE="<from $ARGUMENTS>"
[ -f "$AUDIO_FILE" ] || { echo "ERROR: $AUDIO_FILE not found"; exit 1; }
curl -s -X POST "https://api.groq.com/openai/v1/audio/transcriptions" \
-H "Authorization: Bearer $GROQ_KEY" \
-F "file=@$AUDIO_FILE" \
-F "model=whisper-large-v3" \
-F "response_format=json" | \
jq -r '.text'setup — Configure voice channelsBefore asking for anything, auto-scan ALL sources in one background batch (Rule 4: run_in_background: true):
# Env vars
printenv \
TWILIO_ACCOUNT_SID TWILIO_AUTH_TOKEN TWILIO_FROM_NUMBER \
BLAND_AI_API_KEY ELEVENLABS_API_KEY GROQ_API_KEY \
ZOOM_API_TOKEN ZOOM_ACCOUNT_ID ZOOM_CLIENT_ID ZOOM_CLIENT_SECRET 2>/dev/null
# Shell profiles
grep -hE 'TWILIO|BLAND|ELEVENLABS|GROQ|ZOOM' \
~/.zshrc ~/.bashrc ~/.zprofile ~/.envrc 2>/dev/null | grep -v '^#'
# Doppler — ALL projects
for proj in $(doppler projects --json 2>/dev/null | jq -r '.[].slug'); do
for cfg in dev stg prd; do
doppler secrets --project "$proj" --config "$cfg" --json 2>/dev/null | \
jq -r --arg p "$proj" --arg c "$cfg" \
'to_entries[]
| select(.key | test("TWILIO|BLAND|ELEVENLABS|GROQ|ZOOM"; "i"))
| "\(.key)=\(.value.computed | .[0:12])... (doppler:\($p)/\($c))"'
done
done
# Keychain
for svc in twilio bland-ai elevenlabs groq zoom; do
security find-generic-password -s "$svc" -w 2>/dev/null >/dev/null \
&& echo "[keychain] $svc ✓"
done
# Native macOS prerequisites
ls /Applications/zoom.us.app \
/System/Applications/FaceTime.app \
/System/Applications/Phone.app 2>/dev/nullValidate found keys (in parallel):
| Channel | Probe |
|---|---|
| Twilio | curl -u "$SID:$TOKEN" https://api.twilio.com/2010-04-01/Accounts/$SID.json |
| Bland | curl -H "authorization: $KEY" https://api.bland.ai/v1/me |
| ElevenLabs | curl -H "xi-api-key: $KEY" "https://api.elevenlabs.io/v1/voices?page_size=1" |
| Groq | curl -H "Authorization: Bearer $KEY" https://api.groq.com/openai/v1/models |
| Zoom | curl -H "Authorization: Bearer $TOKEN" https://api.zoom.us/v2/users/me |
Report each as [service] ✓ connected or [service] ✗ <error>. Rule 3 applies — never silently skip a channel. For each unset service, present AskUserQuestion with [Paste manually] / [Deep hunt — spawn agent] / [Skip].
Persist to preferences.json:
{
"channels": {
"voice": {
"backend": "native+twilio+zoom+bland",
"native": { "phone": true, "facetime": true, "zoom": true },
"twilio": { "status": "configured", "from_number": "env:TWILIO_FROM_NUMBER" },
"bland": { "status": "configured" },
"zoom": { "status": "configured" }
}
},
"default_channels": ["whatsapp", "email", "telegram", "slack", "voice"]
}/ops:commsVoice is wired into /ops:comms send-flow. The router resolves intent like:
| User says | Resolves to |
|---|---|
call <name> | ops-voice phone <number> |
facetime <name> | ops-voice facetime <handle> |
start a zoom | ops-voice zoom start |
text <name> "..." | ops-voice twilio-sms ... "..." |
have an AI call <name> and tell ... | ops-voice bland-call <number> "..." |
Contact-number lookup uses the same contact resolver as WhatsApp (mcp__whatsapp__search_contacts) plus an optional contacts.json map in preferences.json.
When $SSH_CONNECTION$SSH_CLIENT$SSH_TTY is set or $OPS_MOBILE=1:
bin/ops-voice still works for API channels.phone, facetime, zoom start|join) require a local macOS session — the script returns a plain-text instruction to open the URL on the host instead of calling open directly. The script must source lib/opener.sh and use ops_open_url for URL handoff.(v1 of bin/ops-voice calls /usr/bin/open directly — Rule 7 adapter is a v1.1 follow-up; tracked in CHANGELOG.)
$ARGUMENTS (first word).bin/ops-voice — exit fast./ops:ops-voice setup.AskUserQuestion → send → next.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.