Skip to content

Release 0.4.1: prevent potential logging of API keys, and other fixes - #159

Merged
dg-coreylweathers merged 34 commits into
mainfrom
release/0.4.1
Nov 12, 2026
Merged

dg-coreylweathers merged 34 commits into
mainfrom
release/0.4.1

Conversation

@dg-coreylweathers

@dg-coreylweathers dg-coreylweathers commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

deepctl 0.4.1

This release prevents potential logging of API keys, and fixes how dg handles credentials, configuration, plugins and local servers.

  • 0.4.1 sends no telemetry. The error-report key that 0.2.21 to 0.4.0 shipped is removed.
  • dg sends your API key only to the host you chose: a regional Deepgram API host, a host you listed in your own config, or DEEPGRAM_BASE_URL.
  • Crash output no longer prints local variables, which could include an API key.
  • A project deepgram.yaml can no longer set credentials, trusted hosts or the programs dg runs.

If you used 0.2.21 to 0.4.0, we recommend upgrading to 0.4.1 and rotating any API key you used with it. If you logged in through your browser, log in again after upgrading.

What's fixed

Credentials and network (deepctl-core, api, login, listen, speak, mcp, debug-probe)

  • dg sends the API key only to a regional Deepgram API host (api.deepgram.com, api.eu.deepgram.com, api.au.deepgram.com or api.in.deepgram.com, and only over https on the default port), a host listed in api.allowed_hosts in the user's own config, or DEEPGRAM_BASE_URL. A DEEPGRAM_BASE_URL set in an AI agent session doesn't count. dg mcp sends the key to api.dx.deepgram.com, or to a --base-url host listed in api.allowed_hosts. Device login (dg login without --api-key) uses id.dx.deepgram.com under the same rule. Any other host is refused before a request is made.
  • REST calls, WebSocket connections (live dg listen, dg speak with Flux, dg debug probe) and dg api no longer follow redirects. Without this change, a 3xx response could pass the key, audio or text on to another host.
  • DEEPGRAM_CLI_INSECURE turns off TLS verification only when it is set to 1/true, only for localhost, 127.0.0.1 and ::1, and never in an AI agent session.

Configuration (deepctl-core)

  • A project deepgram.yaml can no longer set credentials, trust settings or the programs dg runs. Any --config file other than the default config.yaml can no longer set trust settings or the programs dg runs.
  • The config file is written owner-only and atomically. It's locked against concurrent saves. A config that couldn't be read is never overwritten. Oversized or deeply nested YAML is refused.
  • In an AI agent session, dg uses the trust settings in the user config only from the file at its standard location under the real home directory, reached without a symlink, with one hard link, owned by the user and not writable by the group or others. It also doesn't use the API key from a user config that another user owns or the group or others can write.

Plugins (deepctl-core, plugin)

  • A plugin can't replace a built-in command unless the user names it in plugins.allow_override. Plugin command names have to match a strict pattern.
  • dg plugin search lists only plugins that Deepgram publishes.
  • The plugin folders are created private (0700). In an AI agent session, dg uses the plugin environment that dg plugin install manages only from the real home directory, reached without a symlink, and skips it if another user owns its folders or the group or others can write to them. Plugins installed in dg's own Python environment still load.
  • In an AI agent session, dg refuses dg plugin install/update/remove and dg ffprobe --path.

Local servers and tools (mcp, debug-probe, debug-audio, shared-utils, ffprobe, init, install scripts)

  • dg mcp runs over stdio only. The SSE mode, which had no authentication, is removed. Streamable HTTP was never supported. The docs proxy now ships inside deepctl-cmd-mcp (vendored from deepgram-mcp 0.1.1, MIT). It forwards only search_deepgram_knowledge_sources and makes no network connection until a tool is called.
  • dg debug probe accepts connections only from native clients and pages on localhost, 127.0.0.1 or [::1]. Requests from any other origin get a 403 before the upstream connection opens.
  • ffprobe gets its input as file:<absolute path>, so a file name that starts with - is never read as an option. ffprobe itself is looked up from an absolute configured path or from PATH, skipping relative entries.
  • dg init writes .env owner-only, adds it to .gitignore, and clones with --.
  • When install.sh or install.ps1 has to install uv (no uv, pipx or pip found), it installs a pinned uv release and checks its installer against its sha256 before running it.

Telemetry and crash output (telemetry, root)

  • 0.4.1 sends no telemetry. The Sentry key that 0.2.21 to 0.4.0 shipped is removed, and there is no built-in destination, so dg sends no error reports whatever the settings, and doesn't load the Sentry SDK. The error-report code stays for a later opt-in, rebuilt to send only allowlisted fields (command path, flag names, versions, OS, frame locations) with no values; in 0.4.1 it can reach only a loopback DEEPCTL_TELEMETRY_DSN, which exists for tests.
  • dg --help ends with "This version of dg sends no telemetry." The README says the same: no error reports, usage data or other telemetry. dg still contacts the Deepgram API for the commands you run, and pypi.org to check for updates.
  • Crash tracebacks no longer print local variables, which could include an API key.
  • In 0.4.0, dg login, dg projects and dg update could erase a telemetry.enabled: false opt-out, because every config save dropped the telemetry section. 0.4.1 keeps it, so it is still in place if reporting comes back.

Other

  • Upgrade hints and dg update name the deepgram Homebrew formula. The old hint, brew upgrade deepctl, failed.
  • dg no longer splits a hyphenated file name into a command: dg listen debug-call.wav used to read the file as debug call.wav. Only the command name itself is expanded (dg debug-audio still works).
  • dg --help and other output no longer crash when stdout or stderr can't encode a character, as with a redirected or piped Windows console (cp1252). Characters the stream can't write are escaped instead.
  • A config file that can't be looked up (for example a symlink being replaced, or a folder that can't be searched) is reported as unreadable instead of crashing dg. It is never trusted or saved over.

Upgrade notes

Change What to do
dg mcp --transport sse now exits with an error. --transport streamable-http was never supported and still exits with an error. Only stdio is supported. --port and --host are still accepted, and ignored. Remove --transport, --port and --host from your editor's MCP config, or set --transport to stdio.
dg mcp exposes only search_deepgram_knowledge_sources and reads the API key on each call rather than at startup. Nothing, unless you used another remote tool.
deepctl no longer depends on deepgram-mcp; the docs proxy ships inside deepctl-cmd-mcp. A pip or pipx upgrade leaves the old deepgram-mcp package and its deepgram-mcp command installed (with pipx, inside deepctl's environment, not on your PATH). uv tool upgrade deepctl removes it. pip: pip uninstall deepgram-mcp. pipx: pipx runpip deepctl uninstall deepgram-mcp. Point editor configs at dg mcp.
The API key goes only to api.deepgram.com, api.eu.deepgram.com, api.au.deepgram.com, api.in.deepgram.com (over https on the default port only), hosts in api.allowed_hosts, or DEEPGRAM_BASE_URL. A DEEPGRAM_BASE_URL set in an AI agent session doesn't count. dg mcp sends it to api.dx.deepgram.com or a host in api.allowed_hosts, and dg login without --api-key uses id.dx.deepgram.com under the same rule. dg refuses a --base-url or profile that points anywhere else. For a proxy or self-hosted deployment, add its host (optionally host:port) to api.allowed_hosts in your user config.yaml.
A plain http:// base URL is refused unless the host is loopback (localhost, 127.0.0.0/8 or [::1]). A loopback base URL from --base-url or a profile must also be listed in api.allowed_hosts. Only DEEPGRAM_BASE_URL is exempt, and not in an AI agent session. Use https, or a local server on localhost, 127.0.0.1 or [::1]. For a local server, add its host to api.allowed_hosts, or set DEEPGRAM_BASE_URL outside an AI agent session.
A project deepgram.yaml is read only for output.* settings and a regional Deepgram base_url. Everything else is ignored with a warning: api_key, project_id, the active/default profile, other base_urls, and the api, tools, telemetry, update and plugins sections. Move those settings to your user config.yaml, or set them with env vars or flags.
Any --config file other than the default config.yaml loses its api, tools, plugins and update sections, and any non-regional base_url. It can still hold credentials and profiles. Same as above.
A --config path that isn't a regular file (a FIFO, <(...) or /dev/stdin) is no longer read. dg warns and runs without it. Pass a regular file.
In an AI agent session, dg doesn't use the API key or the trust settings from a user config that another user owns or the group or others can write. It still reads the file, but drops its API keys and dg_token_expires_at, and handles its api, tools, plugins and update sections and any non-regional base_url as it does for a --config file. chmod go-w the file and its folder, or chown it to yourself.
In an AI agent session, a user config managed by a dotfile manager loses its trust settings: a symlinked file or folder on the way from the real home directory, a file with more than one hard link, or a file found through HOME or XDG_CONFIG_HOME instead of the real home. dg keeps using its API key, but drops api.allowed_hosts (the whole api section), tools, plugins, update and any non-regional base_url. Make the file at the standard location under your real home directory a regular file with one link, or set those values outside an AI agent session.
DEEPGRAM_CLI_INSECURE works only for 1/true, only for localhost/127.0.0.1/[::1], and never in an AI agent session. Use it only against a local dev server.
0.4.1 sends no telemetry. DEEPCTL_TELEMETRY_DISABLED and telemetry.enabled are still accepted, and the new --no-telemetry flag is accepted too, but none of them has an effect in this version. DEEPCTL_TELEMETRY_DISABLED now reads any non-empty value except 0, false, no and off (in any case) as off; before, only 1, true and yes did. Nothing. You can leave these settings in place.
DEEPCTL_TELEMETRY_DSN is honored only for a loopback host, for tests. Any other value is ignored. There is no built-in destination. The README section on overriding the destination was removed, and the README now says the variable is ignored unless it points at the local machine. Nothing: 0.4.1 sends no reports, so a fork or self-hosted Sentry gets none either.
Plugin command names must match ^[a-z][a-z0-9-]{0,63}$. A plugin can't replace a built-in command: dg runs the built-in and prints a warning. A plugin whose entry point is named like a built-in command is skipped before it's imported. Rename the entry point. To let a trusted plugin replace one, add the command to plugins.allow_override in your user config.yaml (see README, "Plugins").
dg plugin search lists only plugins that Deepgram publishes. Install third-party plugins by package name.
In an AI agent session, dg refuses dg plugin install/update/remove and dg ffprobe --path. Run them yourself in a terminal.
A configured ffprobe path must be absolute. Relative and current-directory entries on PATH are skipped. Run dg ffprobe --path /absolute/path/to/ffprobe again if needed.
dg login no longer saves --base-url or DEEPGRAM_BASE_URL to the profile; it prints that the URL was used for this login only. To save it, run dg login --save-base-url with the URL and confirm at a terminal. For a non-Deepgram host, this also adds the host to api.allowed_hosts.
dg api resolves relative paths against the profile's base URL, not always api.deepgram.com. An absolute URL to a host that isn't trusted is sent without your key. Pass --allow-foreign-host=<host> to include the key. It must match the request host and is refused in an AI agent session.
REST and WebSocket requests no longer follow redirects. A 3xx response now fails with an error that names both origins. dg api instead prints a warning that names both origins, then prints the 3xx response. Use the final URL directly.
dg debug probe refuses browser pages that aren't on localhost/127.0.0.1/[::1] (403). Serve the page from localhost.
The minimum versions rise for mcp (1.22), websockets in deepctl-cmd-listen (13.1), aiohttp in deepctl-cmd-debug-probe (3.11) and sentry-sdk in deepctl-telemetry (>=2.35.0,<3). sentry-sdk stays a dependency only for a later opt-in; 0.4.1 never imports it unless a loopback test DSN is set. Nothing for a normal upgrade.
A ClickException or UsageError raised inside a command is no longer printed after a "Command failed:" line; only the Error: line remains. Exit codes are unchanged. Nothing, unless a script matches on "Command failed:".
When install.sh or install.ps1 has to install uv (no uv, pipx or pip found), it installs uv 0.12.24, checked against a pinned sha256, instead of the latest uv. The new DEEPCTL_UV_INSTALLER_URL variable points either script at a mirror of the uv installer, which must serve the same file. Behind a firewall, set DEEPCTL_UV_INSTALLER_URL to a mirror of uv 0.12.24's installer.

Test results

CI on this PR runs the full matrix, Python 3.10 to 3.14 on each OS, with 0 failures:

  • Linux: 3,382 passed, 9 skipped.
  • macOS: 3,383 passed, 9 skipped.
  • Windows: 2,716 passed, 676 skipped. The skipped tests are POSIX-only: file modes, POSIX paths and config layout, symlinks and fork.
  • Lint, types, dependency floors, the build and CodeQL pass.

Sub-package floors on deepctl-core are raised to >=0.3.1 by hand on the release PR, because the release sync job (check_dependency_floors.py --fix) only moves the root floors. The packages that import a deepctl-core API new in 0.3.1 are deepctl-cmd-api, deepctl-cmd-debug-audio, deepctl-cmd-debug-probe, deepctl-cmd-ffprobe, deepctl-cmd-init, deepctl-cmd-listen, deepctl-cmd-login, deepctl-cmd-mcp, deepctl-cmd-plugin, deepctl-cmd-projects, deepctl-cmd-update and deepctl-telemetry.

Known follow-ups

🤖 Generated with Claude Code

@GregHolmes GregHolmes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes

[B1] OpenCode sessions are not recognized as AI-agent sessions. With OPENCODE=1, an environment-defined DEEPGRAM_BASE_URL=https://evil.example passes the new host gate and can receive the user's API key.

[B2] A rejected symlinked default config still supplies its API key in an AI session. The trust-root check rejects the link, but the fallback rereads it through the symlink and treats a current-user 0600 target as credential-trusted.

I also found a whitespace-check follow-up in the new CRLF config.py lines.

Verified: the PR CI is green and 2,750 focused local security tests pass, but neither blocker is covered by those tests.

dg-coreylweathers added a commit that referenced this pull request Nov 10, 2026
Bring cli#159 into 0.5.0. 0.5.0 keeps its agents table: output.py takes
AI_AGENT_ENV_MARKERS from agents.HARD_ENV_VARS, which already covers
AGENT and CURSOR_EXTENSION_HOST_ROLE. OPENCODE_CLIENT becomes a hard
signal there (any value), as #159 requires for the base-URL key gate.
@dg-coreylweathers

Copy link
Copy Markdown
Contributor Author

Both blockers are in e5aed1f. B1: OPENCODE and OPENCODE_CLIENT are agent markers, with a test covering evil.example. B2: a refused trust root is never reread, with tests for symlinked file, folder and hard link. Whitespace: cr-at-eol in .gitattributes, git diff --check is clean. Re-requesting review.

dg-coreylweathers and others added 19 commits November 12, 2026 11:10
- Turn telemetry off for every test from the root conftest.
- Point the reporting endpoint at a closed local port, so a test that
  runs main() can never reach a real endpoint.
- Telemetry's own tests clear the switch to cover the default.
…to a command

dg turns `dg debug-audio` into `dg debug audio`. It did that to every
argument that began with a command group's name and a hyphen, wherever
it appeared, so `dg listen debug-call.wav` read the file as
`debug call.wav`. A root flag such as --quiet also took the next word
as its value, so `dg --quiet debug-audio` wasn't expanded.

- Expand only the command name, after the root options, and keep every
  argument after it as typed.
- Read root options the way click does: flags take no value,
  --opt=value and -Xvalue carry theirs, and a short cluster such as
  -qv or -qpprod is read letter by letter.
- Stop at --.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Send no telemetry. 0.4.1 has no built-in destination: the Sentry DSN
  that 0.2.21 to 0.4.0 used is removed, so dg sends no error reports,
  sessions, traces, profiles or logs, whether a command succeeds or
  fails. Without a destination, no error hooks are installed and the
  reporting SDK is never imported.
- Accept DEEPCTL_TELEMETRY_DSN only for a loopback address, for tests.
  Any other value is ignored, and the README now says so.
- Keep the error-report code for a later opt-in, reachable only through
  that override: one report per process, and only for an error a
  command didn't raise on purpose. Each report is built from an
  allowlist (command path, flag names, versions, OS and frame
  locations), with no values and no local variables. The SDK loads only
  when there is something to send, with its optional features off, no
  proxy and a short time limit. The empty proxy settings keep a report
  to a loopback address from leaving the machine through a proxy set in
  the environment or the system settings.
- A command's own ClickException now passes through BaseCommand as a
  message to the user. A ClickException or UsageError raised inside a
  command is no longer printed after a "Command failed:" line. Exit
  codes are unchanged.
- DEEPCTL_TELEMETRY_DISABLED and telemetry.enabled are still accepted,
  and the new --no-telemetry flag is accepted too, but none of them has
  an effect in this version.
- DEEPCTL_TELEMETRY_DISABLED turns telemetry off for any non-empty value
  except 0, false, no and off. Before, only 1, true and yes did.
- Read the telemetry setting from the user config only (the default
  file or a --config file), and treat a config that can't be read as
  opted out. A project deepgram.yaml no longer turns telemetry on or
  off, as it could in 0.4.0.
- Read root options the way click does, so root --help honors
  --no-telemetry and --config in any order.
- dg mcp hands a proxy failure to the same code, without writing to
  stdout.
- End --help with a line that says this version of dg sends no
  telemetry. The README's telemetry section says the same, says that dg
  still contacts the Deepgram API and pypi.org, says the three settings
  above are accepted but have no effect, and says DEEPCTL_TELEMETRY_DSN
  is ignored unless it points at the local machine. Golden files check
  both.
- Describe deepctl-telemetry as error reporting that sends no telemetry
  in this version.
- Raise deepctl-telemetry's floors to sentry-sdk>=2.35.0,<3 and
  deepctl-core>=0.3.0. sentry-sdk stays a dependency only for a later
  opt-in; 0.4.1 never imports it unless a loopback test DSN is set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Install the rich traceback handler with show_locals=False. It was
  on, so a crash could print local values, such as an API key.
- Keep the rest of the traceback as it was, on stderr.
- Test that a local's value doesn't appear in the output.
- Save only the user's own settings, never values that came from the
  project file or the environment.
- Write the file owner-only and atomically, and restrict a readable
  file that holds a key.
- Never overwrite a config that couldn't be read, and lock so that
  concurrent saves don't lose each other's changes.
- Keep the sections a save doesn't manage, such as
  telemetry.enabled: false. In 0.4.0 every save dropped the telemetry
  section, so dg login, dg projects, dg update and the update
  version-check cache erased that opt-out.
- Don't read a --config path that isn't a regular file, such as a FIFO,
  <(...) or /dev/stdin. dg warns and continues without it.
- Refuse oversized or deeply nested YAML at load, and never quote the
  file's content in an error.
- dg --record-install-method exits 1 when it can't write the config
  file, so the install script doesn't take a failed write for success.
- Print config warnings on stderr even under --quiet, through the new
  print_security_warning.
- Generate the 64 KiB config test files at test time; a test pins
  their sha256.
…nfig file can set

A deepgram.yaml in the current directory, or a --config file from a
cloned repository, could set the API key, the API host, the ffprobe
path and other settings that decide where your key goes or what dg
runs.

- Read only output settings and a regional Deepgram base_url from a
  project deepgram.yaml. Its api_key, project_id and profile
  selection, its api, tools, plugins, update and telemetry sections,
  and any other base_url are ignored, with one warning per key that
  names the key, never its value. Telemetry settings now come only
  from the user config, the environment and --no-telemetry.
- A --config file other than the default config.yaml, even one in the
  same folder, can still hold credentials and profiles, but its api,
  tools, plugins and update sections, and any non-regional base_url,
  are ignored with a warning.
- In an AI agent session, the user config can set those sections and
  a non-regional base_url only when it is the file at its standard
  location under the real home directory, reached without a symlink,
  with one hard link, owned by the user and not writable by the group
  or others. Otherwise, including a file found through HOME or
  XDG_CONFIG_HOME, dg handles those settings as it does for a --config
  file. A file another user owns or the group or others can write also
  loses its API keys and dg_token_expires_at, and dg doesn't save over
  it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An AI coding agent that runs pytest exports markers such as CLAUDECODE or
AI_AGENT, which put dg into its agent trust mode and failed tests that
expect a person at the keyboard. The root conftest now removes those
markers for every test. Tests that cover agent mode set the markers, or
patch is_ai_agent, themselves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
API keys and request data go only to the host the user chose, and
never follow a redirect somewhere else.

- Send credentials only to a regional Deepgram API host, a host in
  api.allowed_hosts, or DEEPGRAM_BASE_URL. Refuse other hosts before
  any request is made.
- Don't trust DEEPGRAM_BASE_URL in an AI agent session: there it must
  name a regional host or one in api.allowed_hosts.
- Ignore DEEPGRAM_CLI_INSECURE in an AI agent session, with a warning,
  so TLS verification stays on.
- Apply the same check in dg login, dg mcp and dg listen.
- dg login checks a DEEPGRAM_CLI_BASE_URL login host the same way,
  against the Deepgram identity host.
- dg login no longer saves --base-url or DEEPGRAM_BASE_URL to the
  profile. It prints that the URL was used for this login only. The new
  dg login --save-base-url saves it after a person confirms at a
  terminal, and for a non-Deepgram host also adds the host to
  api.allowed_hosts.
- In dg api, attach the key to an absolute URL only for a trusted
  host, and don't follow redirects. A request to another host is sent
  without the key, with a warning.
- Add --allow-foreign-host=<host> to dg api. It sends the key to that
  host only when the value matches the request URL's host and the URL
  is https. It is refused in an AI agent session.
- Resolve a relative dg api endpoint, such as /v1/projects, against the
  profile's base URL instead of always https://api.deepgram.com. The
  base URL passes the same host check first.
- Build the SDK client with follow_redirects=False, so REST calls
  (dg listen and dg transcribe on files, dg speak with Aura, dg read
  and the rest) stop at a 3xx instead of sending the audio or text on
  to another host. The error names both origins.
- Open every WebSocket (live dg listen from the microphone or stdin,
  and dg speak with Flux) through one connect in deepctl-core that
  doesn't follow a redirect on the upgrade, to the same origin or
  another. The command fails with exit 1 before any audio or text is
  sent, with wording that matches the dg api redirect note. Without
  it, the key, audio or text could follow a redirect to another host.
- dg speak with Flux now opens /v2/speak itself, with the SDK's
  headers, instead of through the SDK's connect, so it gets the same
  refusal.
- dg debug probe stops its upstream connect at a redirect too. aiohttp
  has no switch for this on ws_connect, so a trace hook ends it after
  the 3xx and before any request to the new location. Its aiohttp
  floor moves to 3.11, the first release with the ClientWSTimeout the
  probe already uses; below that it couldn't connect at all.
- Raise the websockets floor for deepctl-cmd-listen from 11.0 to
  13.1. Live listen now uses the asyncio client, which 13.0 added (on
  11 and 12 it couldn't connect at all), and 13.1 is the first release
  whose client handles a 3xx the same way as every later one.
- Show a regional host in the --base-url help and say other hosts must
  be listed in api.allowed_hosts. The insecure-URL errors name every
  loopback host plain http may use (localhost, 127.0.0.0/8 and [::1]).
- Document which hosts get your API key, api.allowed_hosts and what a
  project deepgram.yaml can't set, in the README and llms-full.txt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The agent-session trust-root tests and the broken-config CLI tests ran
only on Linux because they built ~/.config paths. They now put the user
config where platformdirs does on each platform (Library/Application
Support on macOS), build expected messages with display_path, and count
open descriptors in /dev/fd on macOS. They still skip on Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a relative PATH entry

dg looked ffprobe up with shutil.which, which follows relative PATH
entries such as "." (and on Windows searches the current directory
first), so it could run an ffprobe from the current directory, such as
one shipped in a cloned repository.

- Find ffprobe on PATH from absolute entries only, skipping empty and
  relative entries and the current directory, on every OS.
- Use a configured tools.ffprobe_path only when it is absolute. dg
  ffprobe --path stores the absolute form of the path it's given.
- dg debug audio now honors tools.ffprobe_path and runs ffprobe by its
  absolute path.
- dg ffprobe status and reset find ffprobe on PATH the same way, so
  neither reports an ffprobe from the current directory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Refuse dg ffprobe --path and dg plugin install, update and remove
  in an AI agent session, since they change what later commands run.
- Scripts and CI without an agent marker still work as before.
- llms-full.txt and llms.txt say what an AI agent session is and what
  dg refuses or ignores in one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- dg update runs brew upgrade deepgram, and both upgrade hints name
  the deepgram formula, after brew update.
- The formula is published as deepgram, so `brew upgrade deepctl`
  failed for Homebrew users.
- Update the installation and update command tests to match.
- Name it in the llms-full.txt install list too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Reserve built-in command names for their own modules. A plugin that
  claims one isn't registered, and dg names its package in a warning.
- Require plugin command names to match ^[a-z][a-z0-9-]{0,63}$, so a
  plugin can't use a name that looks like a built-in.
- Let plugins.allow_override in the user config name a plugin that may
  replace a built-in. It prints a notice each time it runs.
- List only plugins Deepgram publishes in dg plugin search.
- In an AI agent session, use the plugin environment that dg plugin
  install manages only from the real home directory, never one found
  through HOME, and skip it when it is reached through a symlink or
  lies inside a working directory other than the home directory.
  Plugins installed in dg's own Python environment still load.
- Refuse a plugin entry point named like a built-in or reserved
  command by that name, before its module is imported, unless
  plugins.allow_override lists it. A subplugin listed as "debug audio"
  passes. A plugin refused for the name its class sets, or for an
  invalid name, is still imported and then dropped.
- Show plugin names and metadata as plain text, with control
  characters replaced and Rich markup escaped (safe_text.py).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Check Host and Origin before the WebSocket handshake and before any
  upstream connection is opened with the API key.
- Require a Host header of 127.0.0.1:PORT or localhost:PORT, where
  PORT is the probe's own port.
- Accept no Origin (native clients) or a page on localhost, 127.0.0.1
  or [::1]. Refuse everything else with 403.
- Native clients that send one of those Host headers, and local pages,
  work as before, with no new options.
…tions

dg debug audio and the shared ffprobe helper pass ffprobe its input as
the absolute path with a file: prefix, so a file name that starts
with - is no longer read as an ffprobe option. ffprobe reports the
name it was given, so dg debug audio -o json or -o yaml now shows
audio_info.format.filename with the file: prefix.
--ffprobe-args is now split with shlex, like a POSIX shell on every OS,
so a quoted value stays one argument.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Other values keep TLS verification on, and so do hosts other than
localhost, 127.0.0.1 and [::1]. When the variable is set and a request
goes to another host, dg warns once for that host that its certificate
is still verified.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dg mcp --transport sse now exits with an error before reading
credentials or opening a port. streamable-http was never served: 0.4.0
already rejected it, and it now gets the same error. Remove --transport
from your editor's MCP config to use stdio.

python -m deepctl_cmd_mcp refuses both the same way, matched
case-insensitively, instead of failing on streamable-http with a usage
error. The READMEs, llms-full.txt and the landing page drop the SSE
examples.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion

dg init ran git clone with the template URL as its first argument, so
a URL that starts with - was read as a git option. dg now passes the
URL after --, and git takes it as the repository.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dg init writes your API key to .env in the new project.

- Create .env owner-only (0600) at any umask, and narrow an existing
  .env to 0600 when dg writes the key or finds it already set.
- Add .env to .gitignore unless git already ignores it, asking git
  check-ignore when it can.
- Refuse to write through a .env or .gitignore symlink, since the
  cloned template controls its links.
- Warn when the template already tracks .env, since ignoring a tracked
  file doesn't keep the key out of a commit, and suggest
  git rm --cached .env.
- Report a .env that can't be written as a warning, as for an auth
  failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dg-coreylweathers and others added 14 commits November 12, 2026 11:10
When install.sh or install.ps1 has to install uv (no uv, pipx or pip
found), it now installs uv 0.12.24 instead of the latest release, and
checks its installer against the sha256 pinned for that release
before running it. The new DEEPCTL_UV_INSTALLER_URL variable points
either script at a mirror of the installer, which must serve the same
pinned file. A forced reinstall through pip now runs pip with -U
--force-reinstall.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Create ~/.deepctl/plugins and the plugin venv as 0700 at any umask,
  and narrow folders an earlier version left open on the next install.
- In an AI agent session, load no plugins from folders that another
  user owns or others can write, and warn once.
- Outside an agent session, warn and still load the plugins.
…ol is called

dg mcp ran deepgram-mcp's proxy, which connected to Deepgram's
documentation search before answering initialize and passed the
remote's tool list through. The stdio server now lives in
deepctl_cmd_mcp.proxy, vendored from deepgram-mcp 0.1.1 (MIT):

- initialize and tools/list come from fixed local text; nothing
  connects until a tools/call, and each call opens its own session;
- a refused key, no key or a remote that is down is an isError
  result for that call, and the server keeps running;
- only search_deepgram_knowledge_sources is forwarded;
- the HTTP client follows no redirects (same-origin ones included),
  keeps no cookies and verifies TLS;
- stdout carries only JSON-RPC: fd 1 points at stderr while the
  server runs.

dg mcp no longer checks the key at startup; it is read on each call.
The deepgram-mcp dependency is gone, so a fresh install has no
deepgram-mcp console script. mcp's floor rises to 1.22, the oldest
release that runs this server with current pydantic and takes an
httpx client factory. uv.lock drops deepgram-mcp.

dg mcp --help, its messages, the package description and the docs now
describe an MCP server that gives your AI editor Deepgram documentation
search over stdio. The editor setup examples pass DEEPGRAM_API_KEY
through the env block instead of writing the key into .mcp.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The web page's plugin card ran dg plugin install deepctl-cmd-studio,
a package that isn't on PyPI, and called it a community plugin. It now
installs deepctl-plugin-example, the example plugin Deepgram
publishes, under the title "Install a plugin".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README said CLI flags beat environment variables, which beat the
profile config, which beat the project config, for every setting.

- A project deepgram.yaml is read after the user config, so the
  settings it may still set (output.* and a regional base_url) win
  over the user config. The order is now CLI flags, environment
  variables, the project deepgram.yaml, then the user config.
- The API key has its own order: --api-key, the key dg login saved in
  the system keyring, DEEPGRAM_API_KEY, then an api_key in the config
  file. The README and llms-full.txt now say so.
- llms-full.txt no longer names a version. It said 0.2.4, and nothing
  updates it on release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…command

The plugin example README says a plugin whose entry point is named like
a built-in command, such as login, is skipped before it's imported,
unless plugins.allow_override in the user config lists it. It links to
the main README's Plugins section by its GitHub URL, so the link also
works on the package's PyPI page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows, platformdirs ignores HOME and XDG_CONFIG_HOME, so the
opt-out tests read and wrote the real %LOCALAPPDATA% config. Point it at
tmp_path with WIN_PD_OVERRIDE_LOCAL_APPDATA, in-process and in the probe
subprocesses, and give the probes a tmp USERPROFILE.

Stub the update and skills startup checks in telemetry_on, as
test_plugin_override_notice.py does, so the tests stop writing the real
~/.cache/deepctl.

Use os.devnull instead of "/dev/null". On Windows, Click rejected
"/dev/null" as a missing path, so the [dev-null] case passed without
running. Skip the /dev/stdin and bash process-substitution cases on
Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows the agent trust root comes from SHGetFolderPathW, not
_real_home, so the TestAllowOverride tests made folders in, and could
write allow_override into, the real %LOCALAPPDATA% config. A new
_set_real_home helper patches both, so no test reaches the real home.

Also on Windows:
- build the sidecar venv with the Lib/site-packages layout the loader
  expects
- build POSIX-looking test paths with os.path so the expected hint text
  matches
- when the account can't create symlinks (WinError 1314), fall back to
  a directory junction instead of skipping. A non-admin can make a
  junction, and the agent venv checks refuse it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Point HOME and USERPROFILE at tmp_path in the base URL tests. On
  Windows tmp_path sits under the real home, so messages showed "~\..."
  instead of the full path the tests expect.
- Write the user config text test's file as bytes, so Windows doesn't
  turn \n into \r\n.
- Run the concurrent-save child with spawn where fork doesn't exist.
  POSIX still uses fork.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Build absolute paths with os.path.abspath. Python 3.13+ on Windows
  doesn't count "/opt/..." as absolute.
- Expect "file:" plus the native path.
- On Windows, give the fake ffprobe an .exe name and set PATHEXT, so
  the PATH lookup tests run there. Before, the current-directory and
  relative-entry checks passed whether or not the code skipped those
  entries.
- Read the file_info timestamp in UTC so the test passes in any time
  zone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeQL's YAML extractor recurses once per nesting level and ran out of
stack on block_dash_deep.yaml, which failed the Analyze (python) job.
Build that file and the other four nested thousands deep
(deep10k_flow_map, deep10k_flow_seq, quote_bypass_dq, quote_bypass_sq)
at test time, like the three 64 KiB bombs, and check each against the
sha256 of the committed original. The tests still run every bomb in
every placement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows a redirected or piped stdout uses the ANSI code page (cp1252,
say). The --help banner's block art isn't in it, so `dg --help > file`
and `dg --help | more` stopped with "'charmap' codec can't encode". A
stdout or stderr that isn't UTF-8, and whose error handler raises on
such characters (strict, or surrogateescape under a C locale), now
writes them as backslash escapes. The encoding itself is kept, so a real
console keeps the one Python chose, and a UTF-8 stream is untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…crashing

On macOS, stat() on a symlink that is being replaced can fail with
EINVAL, and on Python 3.13 and earlier Path.exists() passes that error
on, so dg stopped with an OSError traceback before it read the config.
A folder that can't be searched did the same. When the lookup fails, dg
now tries to read the file anyway: the trust checks run on the file it
opens, and a read that fails is reported like any other unreadable
config, which is never saved over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…trust root

OpenCode sessions (OPENCODE, OPENCODE_CLIENT) weren't recognised as AI
agent sessions, so an environment DEEPGRAM_BASE_URL such as
https://evil.example passed the host gate and could receive the user's
API key. Add them, along with the other agent-set markers: CLAUDE_CODE,
CLAUDE_CODE_IS_COWORK, CODEX_CI, CODEX_THREAD_ID, COPILOT_CLI,
GOOSE_TERMINAL, AUGMENT_AGENT, ANTIGRAVITY_AGENT, AGENT=amp|goose and
CURSOR_EXTENSION_HOST_ROLE=agent-exec. Variables a person's own shell
sets too (CURSOR_TRACE_ID, REPL_ID) stay out.

When the trust-root check refused the default config, the fallback read
it again through its path and kept a current-user 0600 target's API key.
Now a refused trust root is never read again: a file refused before it
was opened (a symlink on the way) is not used at all and saves through
it are refused, and one refused after the checked open (hard links,
owner, writable) is used from that open with its credentials stripped.

Mark the CRLF files this release touches whitespace=cr-at-eol so
git diff --check passes without rewriting their line endings.
Brings in #135 and the npm launcher (#146). Conflicts resolved by keeping
both sides; src/deepctl/main.py keeps install(show_locals=False) from
7098b24 below main's capture_launcher() call.
@dg-coreylweathers
dg-coreylweathers merged commit 3c027af into main Nov 12, 2026
63 checks passed
@dg-coreylweathers
dg-coreylweathers deleted the release/0.4.1 branch November 12, 2026 18:30
@linear-code

linear-code Bot commented Nov 12, 2026

Copy link
Copy Markdown

DG-2809

dg-coreylweathers added a commit that referenced this pull request Nov 12, 2026
Bring the 0.4.1 release into 0.5.0: the @deepgram/cli npm launcher (#146),
the Homebrew bump that merges itself (#135), and the 0.4.1 fixes (#159).

How the two sides fit:

- src/deepctl/main.py is 0.5.0's. Its main() reads and removes
  DEEPCTL_LAUNCHER before deepctl_core is imported and keeps the value in
  LAUNCHER / LAUNCHER_CAPTURED, which deepctl_core._launcher (from 0.4.1)
  and deepctl_cmd_update.channel both read. Crash tracebacks still never
  show frame locals (install(show_locals=False), now installed lazily).
- deepctl-cmd-update is 0.5.0's, which already detects npm and npx
  installs; 0.4.1's duplicate detection there is dropped. The plugin
  command keeps 0.4.1's isolated venv for npm installs, and the skills
  prompt keeps 0.4.1's rule that an npm launch is not one-shot.
- Versions are lockstep at 0.4.1 (what main last released); the plugin
  example stays at its released 0.1.13. release-please now also moves
  npm/package.json with the root version.
- scripts/check_release_bumps.py and its release-bump-gate are removed:
  with lockstep versions every release bumps every package, so no
  package's code can reach PyPI under an already-published version. The
  build job checks lockstep versions once more before publishing.
- release.yml keeps 0.5.0's re-pin jobs, trusted publishing and tag
  recovery, and adds 0.4.1's pinned build tools, fresh-venv PyPI check,
  Homebrew auto-merge and publish-npm. test.yml adds the three npm
  launcher jobs to All checks.
- conftest.py forgets the launcher main() captured between tests, so a
  test that ran main() no longer decides what later launcher tests see.
dg-coreylweathers added a commit that referenced this pull request Nov 12, 2026
#165)

The 0.5.0 release, based on 0.4.1 (#159). Review after 0.4.1, and merge
only after 0.4.1 is released. 88 commits, on 0.4.1's head `55d3e980`.

### Startup, lockstep versions and dependency floors (1–10)

Startup work: `dg --version` and `dg mcp` start without importing click,
commands load from a generated manifest, plugin discovery and help are
cached, and config loads without pydantic on the help path. Also adds
the competitive benchmark harness, versions every first-party package in
lockstep, and raises the dependency floors (click 8.2, caps on runtime
majors).

<details><summary>Commits</summary>

- a6a212a feat(bench): add competitive benchmark harness with gates
- 27f5859 perf(cli): start dg without importing click for --version and
mcp
- 1ccf4a0 perf(core): import deepctl_core names lazily
- a3c698a build(release): version every first-party package in lockstep
- 92d0130 build(deps): require click 8.2 and cap majors of runtime deps
- b7fd286 feat(cli): load commands from a generated manifest
- 745e258 perf(cli): cache plugin discovery and help output
- edab526 refactor(commands): keep network modules out of command
import time
- 394f6c2 perf(core): load config without pydantic on the help path
- 680e5fb perf(cli): render subcommand help from specs

</details>

### Login for agents, and listen, speak and debug streaming (11–28)

`dg login` accepts real API keys and asks for `--yes` instead of
cancelling, and agents can finish device login with `--complete`. Live
`dg listen` finishes cleanly on Ctrl-C and end of input with bounded
memory; `dg speak` streams audio as it arrives and gains `--out-file`
(`-o/--output` for the audio file is deprecated). Agent mode drops
status chrome from stderr. `dg debug network` sends no stored
credentials, debug browser and probe run on websockets, and numpy,
ffmpeg-python and requests are dropped.

<details><summary>Commits</summary>

- a5052b6 fix(login): accept real API keys and ask for --yes instead of
cancelling
- 582c3f0 feat(login): let agents finish device login with --complete
- e80a8a3 perf(core): drop the pre-flight key check and reuse one HTTP
client
- b0e0780 fix(output): drop status chrome from stderr in agent mode
- c58d7b5 fix(listen): finish live streams cleanly on Ctrl-C and end of
input
- 9ddea92 perf(listen): bound live-stream memory and frame latency
- b80e66f fix(listen): keep machine output to the final result
- ee0b1cc perf(speak): stream audio to pipes and players as it arrives
- 361bf3e feat(speak): add --out-file and deprecate -o/--output for the
audio file
- 6244ab9 fix(speak): don't read stdin or write audio to a pipe in
agent mode
- c915891 fix(listen): report a failed stdout write as an output error
- 1550284 fix(cli): treat Ctrl-C while the CLI is loading as cancelled
- 8cc787e perf(listen): stream prerecorded uploads from disk
- ecba215 feat(listen): add --timeout for prerecorded transcription
- 4491eb1 perf(listen): check URL sources without fetching them
- 61eaa62 fix(debug): send no stored credentials from debug network
- 960b0c3 build(deps): drop numpy, ffmpeg-python and requests from
commands
- 9466058 refactor(debug): run debug browser and probe on websockets

</details>

### Credentials resolver, transport, release workflow and update check
(29–36)

Credentials are resolved in one place and every API request goes through
one transport, so the host checks from 0.4.1 apply everywhere. Releases
publish from `release.yml` with trusted publishing. The update check
never waits, detects pipx, uv and npm installs correctly, and reports an
unreachable index as an error.

<details><summary>Commits</summary>

- b9d015a feat(core): resolve credentials in one place
- 1a409f2 fix(core): send API requests through one transport
- 85dedaf fix(debug): take the probe's and network's host from the
transport
- 327547e ci: keep the CLI smoke runs from sending telemetry
- 4d194c0 ci: publish every release from release.yml with trusted
publishing
- 9dc82c1 fix(update): never wait for the update check
- 9c18f1b fix(update): detect pipx, uv and npm installs correctly
- fc32cc3 fix(update): report an unreachable index as an error

</details>

### Telemetry switches, dg init, JSON output and listen output formats
(37–49)

Telemetry switches (`DO_NOT_TRACK`, `CI`, `DEEPCTL_TELEMETRY=log|off`)
are accepted; this version still sends no telemetry. `dg init` clones
only Deepgram templates, keeps repository settings from running
programs, can resume, and needs `--yes` for setup steps outside a
terminal. JSON output off a terminal skips rich. `dg listen` prints the
server body for `-o json`, adds `-o text`, and saves large results to a
file in agent sessions.

<details><summary>Commits</summary>

- a3c4a4a feat(telemetry): honor DO_NOT_TRACK and CI
- 6096c73 perf(telemetry): skip the flush when no report was queued
- f76f032 feat(telemetry): add DEEPCTL_TELEMETRY=log and =off
- 707672e docs(telemetry): publish the report's field schema
- 8823fb0 fix(init): clone only Deepgram templates, with plain names
- 4a762a2 fix(init): keep repository settings from running programs
- bbaa6ba feat(init): resume a rerun in a clone dg init recorded
- a4eefcb feat(init): setup steps need --yes outside a terminal
- 5e85c87 perf(output): write JSON off a terminal without rich
- dd53c99 test(transport): close the shared client after each test
- 1034dba feat(listen): print the server's body for prerecorded -o json
- 3aa3436 feat(listen): add -o text for the transcript alone
- 39b2847 feat(listen): save large results to a file in agent sessions

</details>

### dg mcp, agent mode, CLI and config fixes (50–73)

`dg mcp` shares one proxy and tool table and keeps stdout for JSON-RPC
from the start. AI agents are detected from one documented table, and
`dg help agent-mode` explains agent mode. Warnings stay off stdout,
config fixes (XDG on macOS, locked config files), Flux model naming,
per-OS stdin and microphone help, and coded messages for cancel and
unexpected errors. Ends with review fix-ups (a plugin named like a
built-in alias is refused before import, `dg login --complete` checks
the base URL, test and README fixes).

<details><summary>Commits</summary>

- 0a33a42 refactor(mcp): share one proxy and tool table for every dg
mcp path
- 9b32d50 fix(mcp): keep stdout for JSON-RPC from the start of dg mcp
- 4eee896 docs(mcp): describe dg mcp as the documentation search it is
- eee53c1 feat(core): detect AI agents from one documented table
- 6a74a66 feat(help): explain agent mode with dg help agent-mode
- aefb1ba fix(cli): keep warnings and notices off stdout
- b7d589d style: say what each check does in config and skills comments
- c1dc7e9 fix(cli): escape characters the output encoding can't write
- 4318989 fix(login): show a profile's config-file key in profiles
--list
- 24c31b1 fix(config): count ~/.config on macOS when XDG_CONFIG_HOME
names it
- ac2d323 fix(config): wait for a locked config file once per run
- 337ba3c fix(listen,speak): name Flux models, not API versions
- 37e2414 fix(listen): give each OS its own stdin and microphone help
- b9048ef test: give timing checks room on a busy host
- 26a7890 fix(cli): use coded messages for cancel and unexpected errors
- 7c415cb test(debug-probe): run the ignored-SIGINT check only on Linux
- bf22611 fix(plugin): refuse a plugin named like a built-in alias
before importing it
- 020cec3 fix(login): check the base URL before dg login --complete
saves a key
- 085b54c test: merge the two fixtures that clear AI agent markers
- d19dad9 docs: compare with 0.4.1 in comments, the release before
0.5.0
- 62fe497 build: regenerate the stale package READMEs and run the
generator with uv
- b2e1889 docs(telemetry): say this version sends no telemetry
- 659e71b test(cli): patch both built-in tables in the missing-package
test
- c5511b2 test(mcp): let the docs lint pass the entry point's transport
refusal

</details>

### Review and CI fixes, and the release marker (74–88)

Fixes from the review and from this PR's first CI run. Windows: the
pending device login and `dg init`'s records are saved by path where a
directory can't be opened; `--out-file` and `-o` work without
`os.fchmod`, write in binary mode, and don't take a pipe or console for
another file; the tests now collect and run there. macOS: the agent
trust root follows `~/.config` only where platformdirs does, and the
terminal, config path, socket and signal tests pass. Tests that signal
their own process run in a child pytest process. The help cache key
records the telemetry sink, an agent's state directory is under the
account's real home, an update check that didn't finish is retried after
an hour, `dg mcp` needs mcp 1.23.1, and the tests run against installed
packages at their floors. Ends with `chore: release 0.5.0`, which
carries `Release-As: 0.5.0`.

<details><summary>Commits</summary>

- f8467e8 fix(login): save the pending login and init's records on
Windows
- df1505f fix(speak): write --out-file and -o files correctly on
Windows
- 9935ef2 fix(speak): don't take a Windows pipe or console for another
file
- 6930c53 test: let the 0.5.0 tests run on Windows
- 63762d0 fix(config): on macOS, follow ~/.config only where
platformdirs does
- df1e7da test: make the terminal, config path and socket tests pass on
macOS
- e880751 test(speak): send each test signal to one thread, in a child
process
- fc69d48 build(deps): require mcp 1.23.1 or later for dg mcp
- 5217565 test: let the unit tests run against installed packages at
their floors
- ea7c121 fix(cli): key cached root help on the telemetry sink setting
- 4ebe797 fix(init): keep an agent's resume records and logins under
the real home
- dfc9e9b fix(update): retry an update check that didn't finish after
an hour
- d8f8096 test(debug-probe): check the Origin refusal on a running
proxy
- cedf89c test(cli): run 0.4.1's output encoding checks on 0.5.0
- 2e97002 chore: release 0.5.0

</details>

### Upgrade notes

- **Telemetry:** none is sent, as in 0.4.1. `DO_NOT_TRACK`, `CI` and
`DEEPCTL_TELEMETRY=log|off` are accepted, and only affect a local
loopback sink set with `DEEPCTL_TELEMETRY_DSN`, which is for tests.
`--no-telemetry` is still accepted and documented as doing nothing.
- **Versions:** every package moves to 0.5.0 together;
`deepctl-telemetry`, for example, goes from 0.0.7 to 0.5.0. Each package
pins `deepctl==0.5.0`. Upgrading one `deepctl-cmd-*` now upgrades the
whole set, or fails to resolve, and an older `deepctl-cmd-*` can't be
mixed with a newer `deepctl`. The packages no longer get their own
GitHub releases or tags.
- **`--version`:** prints the name it was run as: `dg, version 0.5.0` or
`deepgram, version 0.5.0` (0.4.x printed `deepctl, version …` for every
console script). `deepctl` and `python -m deepctl` still print `deepctl,
version …`.
- **listen:**
- `-o json` and `-o yaml` print the server's response body, for
everyone, not only in agent sessions. A 2xx response whose body isn't a
JSON object now exits 1. `DEEPCTL_LISTEN_JSON=sdk` brings back 0.4.x's
SDK-shaped JSON for prerecorded audio.
- Live `-o json` and `-o yaml` print only the final result, not every
interim message.
- In an agent session, `dg listen` with no source (no file, URL or
`--stdin`) is refused instead of opening the microphone.
- URL sources are no longer fetched or checked before the request;
`--check-url` asks for the check.
- Without the pre-flight key check, a bad or missing key is reported
from the API's answer, so those error texts changed.
- **speak:** `-o/--output FILE` is deprecated in favor of `--out-file`.
`-o FILE` now replaces the file through a temp file, so it never writes
through a symlink.
- **Agent sessions:**
  - no status chrome on stderr, and warnings stay off stdout;
  - large listen results are saved to a file;
  - speak doesn't read stdin or write audio to a pipe;
- `dg init`'s resume records and a pending device login live under the
account's own home directory, not `$HOME`.
- **init:** setup steps need `--yes` when not run from a terminal, and
outside a terminal the app starts only with `--start`. Only Deepgram
templates are cloned.
- **login:**
- `-y/--yes` answers the confirmations, and `-f/--force-write` still
works as the same flag;
- without a terminal, the confirmations return `confirmation_required`
instead of "cancelled";
  - `--complete` finishes an agent's device login.
- **api:** `dg api` sends a `User-Agent`, `X-Fern-Platform` and a
session ID header, only to hosts the key may be sent to.
- **debug network:** sends no stored credentials.
- **Update check:** never waits. A check that failed, or didn't finish
before dg exited, is tried again after an hour instead of the next day.
- **Dependencies:**
- click >= 8.2, with caps below the next major of click, httpx,
pydantic, rich, platformdirs, keyring and websockets;
  - `deepctl-cmd-mcp` needs mcp >= 1.23.1 (was 1.22.0);
- numpy, ffmpeg-python, aiohttp and typer are no longer installed, and
the commands no longer depend on requests. The `deepctl` package itself
still lists `requests`.
- **Plugin API:** `DeepgramClient.ws_base_url()` is removed, and
`deepctl_core.websocket.RedirectRefused` is now the transport's error,
no longer a `click.ClickException`.
- **Releases:** published from `release.yml` with trusted publishing.
This needs the `release` environment and the release GitHub App
(`RELEASE_APP_CLIENT_ID` and `RELEASE_APP_PRIVATE_KEY`) set up before
the 0.5.0 release PR.

### Notes

- Tests on this head: Docker (Linux) Python 3.10 5,648 passed and Python
3.14 5,649 passed, 0 failed; lint, types, lock, floors, READMEs and
build pass. The lowest-direct-dependencies job passes on Python 3.10 and
3.12 in Docker. macOS (local) Python 3.10 5,406 passed and Python 3.14
5,407 passed, 0 failed, and the signal and config race tests passed 30
runs in a row on each. Windows can't be run locally: the Windows paths
are tested on every OS by taking away what Windows lacks, and CI on this
PR runs the real matrix.
- After 0.4.1's release PR merges, this branch is rebased onto main and
the lockstep versions are re-baselined to the versions that release set.
- Merge with a merge commit, not a squash: release-please reads the
individual commits. The last commit carries `Release-As: 0.5.0`; without
it, release-please would propose a patch release instead of 0.5.0.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants