Repository navigation
Release 0.4.1: prevent potential logging of API keys, and other fixes - #159
Open
dg-coreylweathers wants to merge 32 commits into
Open
dg-coreylweathers wants to merge 32 commits into
dg-coreylweathers wants to merge 32 commits into
Conversation
- 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>
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>
This was referenced Nov 7, 2026
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>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
deepctl 0.4.1
This release prevents potential logging of API keys, and fixes how dg handles credentials, configuration, plugins and local servers.
DEEPGRAM_BASE_URL.deepgram.yamlcan 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)
api.deepgram.com,api.eu.deepgram.com,api.au.deepgram.comorapi.in.deepgram.com, and only over https on the default port), a host listed inapi.allowed_hostsin the user's own config, orDEEPGRAM_BASE_URL. ADEEPGRAM_BASE_URLset in an AI agent session doesn't count.dg mcpsends the key toapi.dx.deepgram.com, or to a--base-urlhost listed inapi.allowed_hosts. Device login (dg loginwithout--api-key) usesid.dx.deepgram.comunder the same rule. Any other host is refused before a request is made.dg listen,dg speakwith Flux,dg debug probe) anddg apino longer follow redirects. Without this change, a 3xx response could pass the key, audio or text on to another host.DEEPGRAM_CLI_INSECUREturns off TLS verification only when it is set to1/true, only for localhost, 127.0.0.1 and ::1, and never in an AI agent session.Configuration (deepctl-core)
deepgram.yamlcan no longer set credentials, trust settings or the programs dg runs. Any--configfile other than the defaultconfig.yamlcan no longer set trust settings or the programs dg runs.Plugins (deepctl-core, plugin)
plugins.allow_override. Plugin command names have to match a strict pattern.dg plugin searchlists only plugins that Deepgram publishes.dg plugin installmanages 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.dg plugin install/update/removeanddg ffprobe --path.Local servers and tools (mcp, debug-probe, debug-audio, shared-utils, ffprobe, init, install scripts)
dg mcpruns over stdio only. The SSE mode, which had no authentication, is removed. Streamable HTTP was never supported. The docs proxy now ships insidedeepctl-cmd-mcp(vendored from deepgram-mcp 0.1.1, MIT). It forwards onlysearch_deepgram_knowledge_sourcesand makes no network connection until a tool is called.dg debug probeaccepts 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.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 fromPATH, skipping relative entries.dg initwrites.envowner-only, adds it to.gitignore, and clones with--.install.shorinstall.ps1has 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)
DEEPCTL_TELEMETRY_DSN, which exists for tests.dg --helpends 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.dg login,dg projectsanddg updatecould erase atelemetry.enabled: falseopt-out, because every config save dropped thetelemetrysection. 0.4.1 keeps it, so it is still in place if reporting comes back.Other
dg updatename thedeepgramHomebrew formula. The old hint,brew upgrade deepctl, failed.dg listen debug-call.wavused to read the file asdebug call.wav. Only the command name itself is expanded (dg debug-audiostill works).dg --helpand 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.Upgrade notes
dg mcp --transport ssenow exits with an error.--transport streamable-httpwas never supported and still exits with an error. Only stdio is supported.--portand--hostare still accepted, and ignored.--transport,--portand--hostfrom your editor's MCP config, or set--transporttostdio.dg mcpexposes onlysearch_deepgram_knowledge_sourcesand reads the API key on each call rather than at startup.deepgram-mcp; the docs proxy ships inside deepctl-cmd-mcp. A pip or pipx upgrade leaves the olddeepgram-mcppackage and itsdeepgram-mcpcommand installed (with pipx, inside deepctl's environment, not on your PATH).uv tool upgrade deepctlremoves it.pip uninstall deepgram-mcp. pipx:pipx runpip deepctl uninstall deepgram-mcp. Point editor configs atdg mcp.api.deepgram.com,api.eu.deepgram.com,api.au.deepgram.com,api.in.deepgram.com(over https on the default port only), hosts inapi.allowed_hosts, orDEEPGRAM_BASE_URL. ADEEPGRAM_BASE_URLset in an AI agent session doesn't count.dg mcpsends it toapi.dx.deepgram.comor a host inapi.allowed_hosts, anddg loginwithout--api-keyusesid.dx.deepgram.comunder the same rule. dg refuses a--base-urlor profile that points anywhere else.host:port) toapi.allowed_hostsin your userconfig.yaml.http://base URL is refused unless the host is loopback (localhost, 127.0.0.0/8 or [::1]). A loopback base URL from--base-urlor a profile must also be listed inapi.allowed_hosts. OnlyDEEPGRAM_BASE_URLis exempt, and not in an AI agent session.api.allowed_hosts, or setDEEPGRAM_BASE_URLoutside an AI agent session.deepgram.yamlis read only foroutput.*settings and a regional Deepgrambase_url. Everything else is ignored with a warning:api_key,project_id, the active/default profile, otherbase_urls, and theapi,tools,telemetry,updateandpluginssections.config.yaml, or set them with env vars or flags.--configfile other than the defaultconfig.yamlloses itsapi,tools,pluginsandupdatesections, and any non-regionalbase_url. It can still hold credentials and profiles.--configpath that isn't a regular file (a FIFO,<(...)or/dev/stdin) is no longer read. dg warns and runs without it.dg_token_expires_at, and handles itsapi,tools,pluginsandupdatesections and any non-regionalbase_urlas it does for a--configfile.chmod go-wthe file and its folder, orchownit to yourself.HOMEorXDG_CONFIG_HOMEinstead of the real home. dg keeps using its API key, but dropsapi.allowed_hosts(the wholeapisection),tools,plugins,updateand any non-regionalbase_url.DEEPGRAM_CLI_INSECUREworks only for1/true, only for localhost/127.0.0.1/[::1], and never in an AI agent session.DEEPCTL_TELEMETRY_DISABLEDandtelemetry.enabledare still accepted, and the new--no-telemetryflag is accepted too, but none of them has an effect in this version.DEEPCTL_TELEMETRY_DISABLEDnow reads any non-empty value except0,false,noandoff(in any case) as off; before, only1,trueandyesdid.DEEPCTL_TELEMETRY_DSNis 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.^[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.plugins.allow_overridein your userconfig.yaml(see README, "Plugins").dg plugin searchlists only plugins that Deepgram publishes.dg plugin install/update/removeanddg ffprobe --path.PATHare skipped.dg ffprobe --path /absolute/path/to/ffprobeagain if needed.dg loginno longer saves--base-urlorDEEPGRAM_BASE_URLto the profile; it prints that the URL was used for this login only.dg login --save-base-urlwith the URL and confirm at a terminal. For a non-Deepgram host, this also adds the host toapi.allowed_hosts.dg apiresolves relative paths against the profile's base URL, not alwaysapi.deepgram.com. An absolute URL to a host that isn't trusted is sent without your key.--allow-foreign-host=<host>to include the key. It must match the request host and is refused in an AI agent session.dg apiinstead prints a warning that names both origins, then prints the 3xx response.dg debug proberefuses browser pages that aren't on localhost/127.0.0.1/[::1] (403).mcp(1.22),websocketsin deepctl-cmd-listen (13.1),aiohttpin deepctl-cmd-debug-probe (3.11) andsentry-sdkin 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.ClickExceptionorUsageErrorraised inside a command is no longer printed after a "Command failed:" line; only theError:line remains. Exit codes are unchanged.install.shorinstall.ps1has 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 newDEEPCTL_UV_INSTALLER_URLvariable points either script at a mirror of the uv installer, which must serve the same file.DEEPCTL_UV_INSTALLER_URLto 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:
Sub-package floors on deepctl-core are raised to
>=0.3.1by 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
deepgram-skills-v1.7.0, also v1.8.0 in fix(core): install deepgram-skills-v1.8.0 by default #154) still tells agents to rundg mcp --transport sse --port 8000, which now fails.🤖 Generated with Claude Code