Skip to content

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

Open
dg-coreylweathers wants to merge 32 commits into
mainfrom
release/0.4.1
Open

dg-coreylweathers wants to merge 32 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

dg-coreylweathers and others added 29 commits November 5, 2026 23:00
- 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>
dg-coreylweathers and others added 3 commits November 6, 2026 21:44
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

No deployments
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.

1 participant