Repository navigation
Added
- Redaction catches encoded and sliced secrets, and names the key. The
shared engine behindqring exec,qring runand the MCP airlock
(src/core/redaction.ts) now also matches each secret's base64 (standard
and URL-safe, any alignment), hex, URL-encoded and JSON-escaped forms, and
any run of 16 or more characters of its credential part: theuser:password
of a connection string, the path and query of a webhook URL, the signature
of a JWT, the body lines of a PEM, the string values of a JSON credential,
the whole value otherwise. Every hit is replaced by
[QRING:REDACTED:<KEY>];[QRING:REDACTED]stays the fallback when the
key is unknown. Limits: encoded forms for values up to 256 characters,
slices for credential parts up to 1024 characters, repeated-character runs
are not sliced. Inside a JSON credential only values under secret-like key
names (key, token, password, private, refresh, ...) or PEM values are
sliced. The airlock matches whole values and their encoded forms but does
not slice: tool results are agent-shaped text, and a sliced match would
let an agent read a secret out one character per call through any tool
that echoes its input (see the threat model). Streaming output now flushes
as soon as it is written instead of lagging by the longest secret (#121,
#122, #123). - MCP resources for introspection, zero new tools.
qring://policy(the
project's policy as the agent may see it),qring://rotation(the
rotate:duelist, most overdue first) andqring://canariesjoin
qring://sessions. Canaries are hidden unless the server starts with
QRING_MCP_EXPOSE_CANARIES=1: which keys are canaries is the thing the
feature protects. The tool count stays at 46 (#131). - Claude Code plugin: always-on rules. The plugin has no rules component
and does not load a plugin-root CLAUDE.md, so the secret-hygiene and
q-ring-workflow rules now arrive through the SessionStart hook on startup,
/clearand compaction (not onresume, where the transcript already has
them). Ready-made copies ship underclaude-code-plugin/rules/for
~/.claude/rules/. Both are generated from the Cursor rules by
plugin:gen-skills, and CI fails on drift (#127). - The 1.0 stability contract.
docs/stability.mdlists what is frozen as
of 0.19 (the CLI, the 46 tool schemas, resource URIs, the.q-ring.json
policy format, the ring format, environment variables, exit codes, both
redaction tokens), what stays experimental, the semver promise from 1.0,
the deprecation policy and the support window (#134). - Audit pass 2 report:
docs/audits/2026-11-audit-pass-2.md, with
forty fixed findings (see Security), the surfaces examined and found
sound, and the design items carried to 0.20 as issues #144 to #157 (#133). QRING_ALLOW_PRIVATE_HOOKSis the canonical name of the private-address
override (the oldQ_RING_spelling still works, see Deprecated).
QRING_SILENCE_DEPRECATIONS=1silences deprecation warnings.
Changed
- The redaction token names the key:
[QRING:REDACTED:DATABASE_URL]. Match
the[QRING:REDACTEDprefix if you grep for it. - The airlock withholds a tool result as an error when no redaction set can
be built, andqring exec/qring runrefuse to spawn in that case,
instead of passing output through unredacted. A single unreadable secret
(for example a template naming a missing key) is skipped with a
counts-only note; it no longer disables redaction for every other secret. - A corrupt
.q-ring.json(not valid JSON, not an object, unreadable) now
fails closed everywhere policy is enforced, and the diagnostic surfaces
report it instead of crashing:qring doctor(which also honours
--project-pathnow),qring policy(exit 1,{ policyError }with
--json),qring statusand the dashboard, the MCPcheck_policytool,
and every MCP resource. qring ci:validate --jsonexits 1 when a secret fails, as documented.
qring mcp approvalshonours the root--jsonflag.- Shell and signal hooks registered over MCP start disabled; arm one with
qring hook enable <id>. - With a
readableKeysallowlist, MCP writes are limited to those keys too. qring exec/qring runrefuse an unknown--profilename.exec_with_secretsover MCP injects only the keys the project manifest
declares, or, without a manifest, only credential-shaped names
(MY_CONFIGandLOG_LEVELare skipped and listed by name in the
result). Declare the keys you need undersecretsin.q-ring.json. The
CLI is unchanged.
Deprecated
Each prints one q-ring: deprecated: line on stderr per process and is
removed in 1.0 (details and replacements in docs/stability.md):
qring approve <key> --list: useqring approvals.- The bare
--jsonshapes ofcontext,ci:validateandpolicy: 1.0
moves them to the{ ok, data }wrapper the other commands use. - Teleport v1 bundles without an
iterfield (made before 0.12.0). - Agent memory stores under the pre-0.14 machine-derived key.
QRING_CANARY_ALERTS_PATH: useQRING_CONFIG_DIR.Q_RING_ALLOW_PRIVATE_HOOKS: useQRING_ALLOW_PRIVATE_HOOKS.
Fixed
- A stale policy could be served. The
.q-ring.jsoncache was keyed on
mtime alone, andclearPolicyCachedid not clear it, so two writes inside
one millisecond (Windows) returned the previous policy. Both caches now key
on the file's content, per path (#132). qring://canariestied a re-planted canary to its predecessor's trip and
deletion events; events now count only when they follow the current
plant, and project, team and org canaries no longer inherit another
project's same-key events.- The Claude Code SessionStart hook built its JSON with
printf %b, which
mangled backslashes; it now usesjq.
Security
Fixed, then disclosed, house style since 0.12.0. All were found in the
internal audit pass 2 (docs/audits/2026-11-audit-pass-2.md, findings C1 to
C40) or on the way to it; none was reported from outside.
policy.mcp.deniedTagsis enforced. The tag rule was validated but
never evaluated, so a secret carrying a denied tag stayed readable,
listable, exportable and deletable over MCP (C3).- MCP writes respect key policy.
set_secret,teleport_unpack,
import_dotenv,generate_secret --saveAs,promote_secret,
rotate_secretandlint_files --fixcould overwrite a key that policy
denies (C4). - MCP shell and signal hooks start disabled.
register_hookcould plant
a persistent shell command outside exec policy, or a signal hook able to
SIGKILL any of the user's processes (C8). - Exec never injects process-control variables. A secret named after a
variable that steers what a program runs turned an allowlisted command into
arbitrary code execution:NODE_OPTIONS,LD_PRELOAD,PATH,
GIT_EXTERNAL_DIFFinsidegit diff,MAKEFLAGSinsidemake, and so
on. Whole families are now denied (GIT_*,SSH_*,MAKE*,
CARGO_*/RUSTC*, the interpreter start-up variables,LD_*/DYLD_*,
the package managers) along withPAGER,EDITOR,HOME,KUBECONFIG
and similar. Credentials inside a family (NPM_TOKEN,
CARGO_REGISTRY_TOKEN,SSH_PRIVATE_KEY) still inject. These names are
never injected over MCP, and from the CLI only when named with--keys.
Exec profiles are not a sandbox: a caller who controls an allowed binary's
arguments can still run code through it (C12, C18). - A corrupt
.q-ring.jsonmeant "no policy". A file that exists but is
not valid JSON used to read as absent, so every deny rule was dropped. It
now fails closed. - Canary identity no longer leaks through
prevHashgaps in MCP
export_audit,qring://sessions/{id}and the dashboard an agent starts
withstatus_dashboard(C5, C32), throughpolicy_denyevents when an
alert channel is SSRF-blocked (C6), or through silent reads in
ci_validate_secrets, which now audits each read and trips canaries (C7). canary plant --forcestarts fresh. It no longer inherits the replaced
secret's stats or settings, and never copies the fake value into entangled
real secrets (C2).- q-ring's master keys stay out of child processes.
QRING_FILE_PASSPHRASEandQRING_MEMORY_PASSPHRASEare stripped from
exec and run children,--inherit-envwrapped servers and push CLIs,
whatever their letter case (C9, C39). - Airlock redaction covers errors, progress and URIs, not only results
(C10). qring pushrefuses values that would inject variables into fly or
netlify (C11).- Unknown exec profiles are an error;
--profile <typo>used to run
unrestricted (C1). - Teleport bundles reject control characters in key names, scopes and
recipient ids (C13). - The file backend writes atomically; a failed write can no longer
destroy the store (C14). - Wrap tool-name globs match across newlines, and the airlock rate
limiter tracks at most 1024 tool names (C15). - SSRF-blocked hook URLs are logged as scheme and host only (C16).
- Environment names are looked up by own property, so
constructorand
similar names never resolve to prototype members (C17). - Entanglement honours
deniedTagsin both directions: MCP writes do not
propagate into a tag-denied key, andentangle_secretsrefuses links that
touch one (C19). - Bulk MCP writers no longer abort halfway.
import_dotenv,
teleport_unpackandlint_files --fixcheck every key first, write the
allowed ones and list the protected ones as skipped with a reason
(importDotenvgainsskippedByPolicy).lint --fixleaves a file
byte-identical when none of its fixes applied and keeps each line's own
line ending when it rewrites one (C20 to C22, C31, C34). agent_scanauto-rotation over MCP honours the MCP write policy.
Protected keys are never rotated and are reported inrotationSkipped
(C23).- Blocked canary alert deliveries are no longer counted as trips. They
are recorded ascanary_alertand hidden from agents (C24). - A delete across scopes checks every copy first, so a policy denial
deletes nothing (C25, C26). - Exec never injects bash functions or network-hijack variables. A key
that is not a plain identifier, such asBASH_FUNC_ls%%(which bash
imports as a function, also as/bin/sh), is never injected. Proxy and TLS
trust-store variables (HTTPS_PROXY,SSL_CERT_FILE,NODE_EXTRA_CA_CERTS
and similar) and the pip, composer, gem, bundler, nix, dotnet and OpenSSL
families are denied like the other process-control names, as are the
TAR_OPTIONS,CFLAGS/LDFLAGS,CGO_*,GOAUTH,MAVEN_OPTS-style
JVM options,RSYNC_RSH,KUBECTL_EXTERNAL_DIFFand the
uv/poetry/conda/gcloud/gh families. A key whose name is not a plain
environment variable name, such asstripe-key, is no longer injected by
exec or run (which now applies the same rule to manifest keys and.env
names); rename it (C27, C36, C37). - Over MCP, exec injects only the keys the project manifest declares.
When.q-ring.jsonlists secrets,exec_with_secretsinjects that set
(intersected withkeyswhen given), read from the pinned project root
rather than a caller-chosen path, the same least-privilege setqring run
uses, so a key an agent planted withset_secretis inert unless the
operator declared it. Without a manifest the tool injects only
credential-shaped names (*_KEY,*_TOKEN,*_SECRET,*_PASSWORD,
*_URL,*_DSNand similar) and lists what it skipped, by name, in its
result; declare a manifest to lift that rule. The denylist above stays as
defence in depth on every path; for the CLI operator path, where the
operator chooses the keys, it is best effort and cannot be complete (C36,
C40). get_project_contextand agent-started dashboards no longer show canary
records. A tripped canary's key no longer appears in their recent
activity (C28).- An MCP write cannot shadow a protected secret from another scope. A
project-scope write is refused when a global (or any other resolvable)
copy of the key is denied by key or tag policy (C29). - Entangled secrets share deny tags. When one side of an entanglement
carries a tag thatpolicy.mcp.deniedTagslists, at link time or on any
later write, the partner takes the tag too, so a link made before a secret
was tagged can no longer keep copying its value into a key an agent can
read (C33). - Re-setting a secret keeps its tags and description.
qring set KEY valuewithout--tagsused to drop the existing tags, so rotating a
protected value from the CLI silently removed itsprodtag and the
deniedTags protection with it.--clear-tags(andclearTagson
set_secret) removes them on purpose; combining it with new tags is
refused on both paths (C35, C38). - Known limitation, documented, not fixed: on Linux a child of exec or
run can read the parent's original environment block through
/proc/<ppid>/environ, passphrases included. A file- or keyring-held
passphrase is planned for 0.20 (q-ring#156).