<p align="center">
<img src="assets/voxport-github-banner.png" alt="Voxport, Notifications for ChatGPT Voice." width="100%">
</p>
<p align="center">
<a href="https://github.com/ItsWendell/voxport/actions/workflows/ci.yml"><img src="https://github.com/ItsWendell/voxport/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<a href="https://github.com/ItsWendell/voxport/releases/latest"><img src="https://img.shields.io/github/v/release/ItsWendell/voxport" alt="Latest release"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/ItsWendell/voxport" alt="MIT license"></a>
</p>
# Voxport
Voxport is an open-source macOS CLI and daemon for sending queued coding-agent
notifications and messages through an actual ChatGPT Voice conversation. The
project ships one Go binary named `chatgpt-voice`.
It can:
- Find the most recently used native Voice task automatically.
- Start a real ChatGPT Voice session without clicking the UI.
- Deliver a source-attributed notification through ChatGPT desktop IPC.
- Queue concurrent updates durably and process urgent items first.
- Start Voice immediately, then combine notifications that arrive during wake.
- Lightly coalesce active-session notifications within a 100 ms window.
- Expire stale notifications, retry transient failures, and retain delivery history.
- Distinguish a user-owned Voice session from one Voxport started.
- Ask the Voice agent to close a transient call after ten seconds without a reply.
- Leave a Voice session open with `--keep-open`.
- Preserve or explicitly set the permission mode for Voice sessions it wakes.
- Preserve or explicitly set the microphone state for Voice sessions it wakes.
- Disable notifications globally or for one process environment.
## Requirements
- macOS
- `/Applications/ChatGPT.app`
- A local Codex or ChatGPT task previously used with native Voice
Go 1.24 or newer is required only when building from source. Voxport does not
require an OpenAI API key.
Voxport currently integrates with private implementation details in the native
ChatGPT app. App updates can require compatibility changes.
## Install
### Install a release
Release archives are available for Apple Silicon and Intel Macs. The following
downloads `v0.3.0`, verifies its checksum, and installs the binary into
`~/.local/bin`:
```bash
set -euo pipefail
VOXPORT_VERSION=0.3.0
case "$(uname -m)" in
arm64) VOXPORT_ARCH=arm64 ;;
x86_64) VOXPORT_ARCH=amd64 ;;
*) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;;
esac
VOXPORT_ARCHIVE="voxport_${VOXPORT_VERSION}_darwin_${VOXPORT_ARCH}.tar.gz"
VOXPORT_RELEASE_URL="https://github.com/ItsWendell/voxport/releases/download/v${VOXPORT_VERSION}"
curl -fLO "${VOXPORT_RELEASE_URL}/${VOXPORT_ARCHIVE}"
curl -fLO "${VOXPORT_RELEASE_URL}/checksums.txt"
grep " ${VOXPORT_ARCHIVE}$" checksums.txt | shasum -a 256 -c -
tar -xzf "$VOXPORT_ARCHIVE"
install -d "$HOME/.local/bin"
install -m 755 \
"voxport_${VOXPORT_VERSION}_darwin_${VOXPORT_ARCH}/chatgpt-voice" \
"$HOME/.local/bin/chatgpt-voice"
VOXPORT_SKILL_TARGET="$HOME/.codex/skills/notify-voice"
if [[ ! -e "$VOXPORT_SKILL_TARGET" ]]; then
install -d "$HOME/.codex/skills"
cp -R \
"voxport_${VOXPORT_VERSION}_darwin_${VOXPORT_ARCH}/examples/codex-skill/notify-voice" \
"$VOXPORT_SKILL_TARGET"
else
echo "Codex skill already exists at $VOXPORT_SKILL_TARGET; leaving it unchanged."
fi
chatgpt-voice daemon install
```
Release binaries are built by GitHub Actions with checksums and provenance
attestations, but are not currently notarized with Apple. See
[`SECURITY.md`](SECURITY.md) for the trust boundary.
### Build from source
Clone the repository, then build and install the binary into `~/.local/bin`:
```bash
make install
make install-skill
chatgpt-voice daemon install
```
`make install-skill` creates a symlink from
`~/.codex/skills/notify-voice` to the example in this repository. It refuses to
replace an existing non-symlink directory.
Make sure `~/.local/bin` is on `PATH`. For zsh:
```bash
export PATH="$HOME/.local/bin:$PATH"
```
The daemon is also started automatically on first delivery if the LaunchAgent
has not been installed.
To remove the LaunchAgent:
```bash
chatgpt-voice daemon uninstall
```
The uninstall command deliberately leaves queued state and configuration in
place.
See [all releases](https://github.com/ItsWendell/voxport/releases) and the
[`CHANGELOG.md`](CHANGELOG.md) for release history.
## Use
```bash
chatgpt-voice notify "Sup!"
chatgpt-voice notify --kind milestone "The build passed."
chatgpt-voice notify --kind input-required \
"Choose staging or production in the source task."
chatgpt-voice notify --kind insight --priority quiet \
"The slowdown is one missing index."
chatgpt-voice message "This is a regular task message."
```
Normal and urgent notifications start Voice when needed. Quiet notifications
are delivered without starting Voice unless `--auto-start` is present.
```bash
chatgpt-voice notify --priority quiet "FYI only"
chatgpt-voice notify --priority quiet --auto-start "Say this now"
chatgpt-voice notify --no-start "Deliver silently"
chatgpt-voice notify --keep-open "Start Voice and leave it open"
chatgpt-voice notify --wake-microphone muted "Start Voice with the mic muted"
chatgpt-voice notify --ttl 30s "Only say this while it is still useful"
chatgpt-voice notify --no-expiry "Keep this queued until it can be delivered"
```
Inspect and control the local system:
```bash
chatgpt-voice status
chatgpt-voice queue
chatgpt-voice history
chatgpt-voice flush
chatgpt-voice start
chatgpt-voice stop
chatgpt-voice daemon status
chatgpt-voice daemon stop
chatgpt-voice config
```
## Delivery scheduling and expiry
Voxport optimizes for fast delivery without waiting for the Voice agent's
current speech or thinking phase:
1. If Voice is inactive, Voxport starts it immediately. Notifications that
arrive while ChatGPT is opening are submitted together as one structured
turn as soon as Voice is ready.
2. If Voice is already active, including a call opened manually, Voxport finds
its task across every ChatGPT window. It checks whether a desktop renderer
currently owns that task. When it does not, Voxport uses ChatGPT's internal
host-routed follow-up request, which does not select the Voice task, change
the visible task, toggle Voice, or restart the call.
3. Voxport waits only the configured 100 ms micro-batch window. Notifications
that arrive in that window are submitted together; later notifications
become separate turns.
4. Regular `message` commands are never batched. Notification batches contain
every original source, kind, priority, and message payload.
Notifications expire after 15 minutes by default. Expiry only removes items
that are still in Voxport's local queue, it cannot retract a turn already
accepted by ChatGPT. Override the policy per notification with `--ttl` or
`--no-expiry`, or persist new defaults:
```bash
chatgpt-voice config set batch-window 100ms
chatgpt-voice config set notification-ttl 15m
```
Delivery failures use bounded exponential backoff. A failed item does not block
another ready notification, and normally moves to history after five failed
attempts. ChatGPT's `no-client-found` response is handled differently: it means
that no connected desktop renderer currently owns the target task. Voxport
probes this before delivery and uses ChatGPT's background host request instead.
If both routes remain unavailable, it repeats ChatGPT's full client discovery
every 500 ms and retains the notification until its TTL instead of discarding
it after five attempts. `chatgpt-voice status` reports the detected active task,
whether its desktop owner is ready or missing, and how many queued items are
waiting for a ChatGPT client. `chatgpt-voice history` shows the last 100
delivered, expired, and failed items, including enqueue-to-accept latency,
attempt count, and batch size.
## Configure wake permissions
Native Voice activation can reapply ChatGPT's managed permission defaults.
Voxport can enforce a selected permission mode after it wakes Voice and before
it starts the notification turn:
```bash
chatgpt-voice config set wake-permissions inherit
chatgpt-voice config set wake-permissions auto
chatgpt-voice config set wake-permissions full-access
chatgpt-voice config show
```
The modes are:
- `inherit`, preserve the Voice task's current mode. This is the default.
- `auto`, allow normal work inside the task's workspace and request approval
for exceptional access.
- `full-access`, allow unrestricted filesystem and internet access without
approval prompts.
The configured mode is applied only when Voxport starts an inactive Voice
session. It does not overwrite a Voice session that was already open. For
`auto` and `full-access`, Voxport first updates the task's next-turn settings,
then includes the same settings explicitly in the delivered turn. This prevents
native Voice startup from silently replacing the requested mode.
`full-access` is intentionally opt-in. It lets ChatGPT run commands, access the
internet, and modify files anywhere on the computer without asking.
## Configure the wake microphone
Voxport can preserve or set the microphone state when it starts an inactive
Voice session:
```bash
chatgpt-voice config set wake-microphone inherit
chatgpt-voice config set wake-microphone muted
chatgpt-voice config set wake-microphone unmuted
chatgpt-voice config show
```
The modes are:
- `inherit`, keep ChatGPT's microphone state. This is the default.
- `muted`, mute the microphone as the native Voice controls become available.
- `unmuted`, unmute the microphone as the native Voice controls become
available.
This setting affects only a Voice session that Voxport starts. If Voice is
already open, Voxport treats it as user-owned and does not change its
microphone. ChatGPT's output remains audible in every mode. Override the saved
setting for one delivery with `--wake-microphone <mode>`. Voxport applies the
state at the earliest available native control, verifies it again before
delivering the notification turn, and rechecks it while it owns the transient
session. ChatGPT can reset the microphone across Voice turn transitions.
Voxport reads the native microphone control before toggling it, so the operation
is idempotent. Because this depends on a private ChatGPT.app command and UI
state, verify compatibility again after a ChatGPT.app update.
## Configure the Voice task
Routing order is:
1. `--thread <task-id>`
2. `CODEX_VOICE_THREAD_ID`
3. ChatGPT's most recently active local Voice task
4. Voxport's saved configuration
5. The legacy `chatgpt-voice` configuration, when present
Save an explicit default:
```bash
chatgpt-voice configure YOUR_VOICE_TASK_ID
```
Configuration is stored at `~/.config/voxport/config.json` by default.
`XDG_CONFIG_HOME` and `XDG_STATE_HOME` are respected.
## Enable or disable notifications
Disable notification deliveries persistently:
```bash
chatgpt-voice notifications disable
chatgpt-voice notifications status
chatgpt-voice notifications enable
```
The environment variable is checked first and overrides persistent config:
```bash
CHATGPT_VOICE_NOTIFICATIONS=off chatgpt-voice notify "Skipped"
CHATGPT_VOICE_NOTIFICATIONS=on chatgpt-voice notify "Forced on"
```
Accepted values are `on`, `off`, `true`, `false`, `yes`, `no`, `1`, and `0`.
The switch applies to `notify`; an intentional `message` command still works.
This split is useful for agent skills. They can avoid even invoking the notify
command when the variable is explicitly off, while the CLI remains the final
policy enforcement point for persistent settings and non-skill callers.
See [`examples/codex-skill/notify-voice/SKILL.md`](examples/codex-skill/notify-voice/SKILL.md).
## How native Voice lifecycle works
ChatGPT does not currently expose Voice start and manual stop through its public
desktop socket. Voxport briefly opens Electron's loopback Node inspector and
invokes ChatGPT's own trusted renderer commands and native microphone control:
- `composer.startVoiceMode`
- `realtimeVoice.endCall`
The microphone setting is read from and applied through ChatGPT's dedicated
`voice-microphone` overlay surface.
Message delivery uses the app's normal framed Unix desktop IPC method,
`thread-follower-start-turn`.
For a manually opened call, Voxport combines two independent observations: the
native Voice overlay establishes that a call is active, while ChatGPT's `Voice
chat active` sidebar marker identifies its task. Voxport scans every main
ChatGPT window for that marker. A read-only desktop IPC probe then distinguishes
a task that can receive a turn through desktop IPC from an active overlay that
needs background host delivery.
The daemon follows this ownership policy:
1. If Voice was already active, the session is user-owned. Voxport never adds a
close instruction or closes it automatically.
2. If Voxport starts Voice, the session is Voxport-owned.
3. The delivered turn asks the Voice agent to wait ten seconds after it finishes
speaking. If the user has not responded, the agent ends its own call. If the
user responds, it continues the conversation and keeps the call open.
4. `--keep-open` omits that lifecycle instruction.
There is deliberately no native idle-timer fallback. The daemon continues to
reconcile the microphone state for a Voxport-owned session and clears its local
ownership record after the Voice agent ends the call.
If the macOS session is locked, start-required items remain queued until the
session is available.
## FAQ
### Is Voxport affiliated with OpenAI?
No. Voxport is an independent open-source project. ChatGPT, Codex, and OpenAI
are trademarks of OpenAI.
### Is this a separate text-to-speech voice?
No. Voxport delivers the turn to a native ChatGPT Voice task and starts the
actual Voice experience in ChatGPT.app when the notification policy calls for
it.
### Does Voxport require an OpenAI API key?
No. It communicates with the local ChatGPT desktop app and the task already
available there.
### Can a ChatGPT.app update break Voxport?
Yes. Voice start and manual stop are not exposed through a public desktop interface,
so Voxport uses private implementation details. Compatibility fixes may be
needed after ChatGPT.app updates. Include the Voxport, macOS, and ChatGPT.app
versions when reporting a problem.
### Why does Voxport open a Node inspector?
Voxport briefly opens ChatGPT's loopback Node inspector to invoke the app's own
trusted Voice start and manual stop commands. It verifies that the inspector belongs
to the expected ChatGPT process before evaluating a command, and closes an
inspector it opened after the operation or daemon session ends. This is still a
private integration surface. Review [`SECURITY.md`](SECURITY.md) before using
Voxport in a sensitive environment.
### Will Voxport close Voice while I am using it?
If Voice was already active, Voxport treats the session as user-owned and never
asks it to close. For a transient session Voxport starts, the delivered turn
asks the Voice agent to end its own call after ten seconds without a response.
If you respond, the conversation continues. `--keep-open` omits the instruction.
### Can a notification start Voice with the microphone muted?
Yes. Run `chatgpt-voice config set wake-microphone muted`, or add
`--wake-microphone muted` to one notification. Voxport changes the microphone
only when it starts an inactive Voice session. It never changes the microphone
on a Voice session that was already open.
### Will music or another app interfere with call cleanup?
Voxport no longer uses output-audio detection to close calls. The Voice agent
decides whether the user has responded. Loud audio reaching an unmuted
microphone can still be treated as input by ChatGPT Voice; use
`wake-microphone: muted` for notification-only sessions when appropriate.
### Can a Voice notification approve permissions?
Voxport can notify you that a source task needs input or permission. Sensitive
or destructive approval should still happen in the source task. The optional
wake-permissions setting controls the target Voice task's execution mode, not a
one-click approval relay.
### What happens when several agents notify at once?
Deliveries enter a durable, priority-aware queue. Notifications arriving during
Voice startup are combined, while an active session uses a 100 ms micro-batch.
The combined turn receives one call-lifecycle instruction.
### Why can Voice be open while a notification is waiting?
The native Voice overlay and the desktop renderer that owns its task can have
different lifecycles. If the renderer reconnects or is recreated, ChatGPT can
temporarily return `no-client-found` even while Voice still looks open. Voxport
keeps the notification queued, searches all connected ChatGPT clients every
500 ms, and delivers as soon as one owns the task again. Check
`chatgpt-voice status`, `chatgpt-voice queue`, and `chatgpt-voice history` to
distinguish waiting, expired, and failed deliveries.
### How do I turn notifications off temporarily?
Use `CHATGPT_VOICE_NOTIFICATIONS=off` for one process environment, or run
`chatgpt-voice notifications disable` for persistent local configuration. A
deliberate `chatgpt-voice message` still works.
## Configuration file
Optional fields:
```json
{
"voiceThreadId": "YOUR_VOICE_TASK_ID",
"notificationsEnabled": true,
"wakePermissions": "inherit",
"wakeMicrophone": "inherit",
"batchWindow": "100ms",
"notificationTtl": "15m"
}
```
## Develop
```bash
go test ./...
go vet ./...
make build
make release-snapshot VERSION=v0.3.0-dev
```
The broker is designed around three boundaries: native Voice control, desktop
delivery, and durable state. Tests use fakes at those boundaries and verify the
public lifecycle behavior.
Bug reports should use the GitHub issue template and omit sensitive task IDs or
notification contents. Report vulnerabilities privately as described in
[`SECURITY.md`](SECURITY.md). See [`CONTRIBUTING.md`](CONTRIBUTING.md) before
opening a pull request.