NousResearch/hermes-agent
Redacting credentials from URLs in output
Cluster 6396 · agent. Data as of
Nothing open on GitHub. Fix merged in #54475 on 29 Jun 2026. Check it covers your case.
The short answer
0open issues
0open PRs
4items grouped
- Known?
- Yes. 4 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #54475. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #54475. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These issues and pull requests concern identifying and redacting embedded credentials (tokens, passwords) from URLs before they appear in surfaced output. The work spans generic URLs, bare-token userinfo fields, and Git URLs specifically.
Open issues0
Open PRs0
Fixes on record
-
#54475 fix(security): redact bare-token credentials in URL userinfo (#6396)
files:
agent/redact.py,tests/agent/test_redact.pyShow the diff
-
agent/redact.py@@ -222,6 +222,28 @@ re.IGNORECASE, ) +# Bare-token credential in a web/transport URL: ``scheme://TOKEN@host``. +# This is the ``git remote set-url origin https://[redacted:email]/...`` +# shape from issue #6396 — a single opaque credential in the userinfo position +# with NO ``user:pass`` colon. It is unambiguously a secret: legitimate +# round-trip URLs (OAuth callbacks, magic links, pre-signed shares — see the +# "Web-URL redaction is intentionally OFF" note in redact_sensitive_text) carry +# their tokens in the QUERY STRING, never in bare userinfo. The colon form +# ``user:pass@`` is deliberately left to pass through (commit "pass web URLs +# through unchanged", #34029) and is NOT matched here — the token class forbids +# ``:``. DB schemes are handled by _DB_CONNSTR_RE above and excluded here. +# +# Guards against false positives: +# - 8+ char floor skips short usernames (git, admin, root, deploy, ubuntu). +# - The token class ``[^\s:@/]`` cannot cross ``/``, so an ``@`` sitting in a +# path or query (e.g. ``?q=[redacted:email]``) is never treated as userinfo. +_URL_BARE_TOKEN_RE = re.compile( + r"((?:https?|wss?|git|ssh|ftp|ftps|sftp)://)" # scheme + r"([^\s:@/]{8,})" # bare token (no colon/slash/@), 8+ chars + r"(@[^\s]+)", # @host... + re.IGNORECASE, +) + # JWT tokens: [redacted:secret-assignment] — always start with "eyJ" (base64 for "{") # Matches 1-part (header only), 2-part (header.payload), and full 3-part JWTs. _JWT_RE = re.compile( @@ -564,6 +586,16 @@ def _redact_db(m): else: text = _DB_CONNSTR_RE.sub(lambda m: f"{m.group(1)}***{m.group(3)}", text) + # Bare-token userinfo in web/transport URLs: ``scheme://TOKEN@host``. + # The git-remote-with-embedded-password shape from #6396. Only the + # colon-less bare-token form is redacted — ``user:pass@`` and + # query-string tokens are left to pass through (see the web-URL note + # below). See _URL_BARE_TOKEN_RE for the false-positive guards. + text = _URL_BARE_TOKEN_RE.sub( + lambda m: f"{m.group(1)}{_mask_token(m.group(2))}{m.group(3)}", + text, + ) + # JWT tokens (eyJ... — base64-encoded JSON headers) if "eyJ" in text: text = _JWT_RE.sub(lambda m: _mask_token(m.group(0)), text) @@ -575,7 +607,12 @@ def _redact_db(m): # blanket-redacting param values by name breaks those skills mid-flow. # Known credential shapes (sk-, ghp_, JWTs, etc.) inside URLs are still # caught by _PREFIX_RE and _JWT_RE above. DB connection-string passwords - # are still caught by _DB_CONNSTR_RE. + # are still caught by _DB_CONNSTR_RE. The ONE userinfo case still redacted + # is the colon-less bare-token form ``scheme://TOKEN@host`` (#6396, handled + # by _URL_BARE_TOKEN_RE in the ``://`` block above): a bare credential in + # userinfo is never a round-trip workflow token (those live in the query + # string), so masking it can't break a skill. The ``user:pass@`` form is + # left to pass through per #34029. # Form-urlencoded bodies (only triggers on clean k=v&k=v inputs). if "&" in text and "=" in text: -
tests/agent/test_redact.py@@ -492,6 +492,79 @@ def test_db_connstr_password_still_redacted(self): assert "dbpass" not in result +class TestBareTokenUserinfoRedaction: + """Regression tests for #6396 — a bare credential in URL userinfo + (``scheme://TOKEN@host``, no ``user:pass`` colon) is redacted. This is the + git-remote-with-embedded-password shape. The colon form ``user:pass@`` and + query-string tokens are deliberately left to pass through (#34029) so + magic-link / OAuth round-trip skills keep working — see + TestWebUrlsNotRedacted for those invariants. + """ + + def test_git_remote_bare_password_redacted(self): + """Exact bug scenario: password in a git remote URL.""" + text = ( + "git remote set-url origin " + "https://[redacted:email]/unclehowell/FCUK.git" + ) + result = redact_sensitive_text(text) + assert "MYPASSWORDWASDISLAYEDHERE" not in result + assert "@github.com" in result + assert "unclehowell/FCUK.git" in result + + def test_ssh_bare_token_redacted(self): + text = "ssh://[redacted:email]/project.git" + result = redact_sensitive_text(text) + assert "longtoken1234567" not in result + assert "@gitlab.com" in result + + def test_ftp_bare_token_redacted(self): + text = "ftp://[redacted:email]/files" + result = redact_sensitive_text(text) + assert "ftptoken123456" not in result + + def test_bare_token_with_query_redacts_token_only(self): + text = "https://[redacted:email]/path?foo=bar" + result = redact_sensitive_text(text) + assert "abcdef1234567" not in result + assert "?foo=bar" in result + + def test_user_pass_form_still_passes_through(self): + """The ``user:pass@`` colon form must NOT be redacted (#34029).""" + text = "URL: https://[redacted-credentials]@host.example.com/path" + assert redact_sensitive_text(text) == text + + def test_short_username_not_redacted(self): + """Short userinfo (git, admin, deploy) below the 8-char floor passes.""" + for text in ( + "https://[redacted:email]/user/repo.git", + "https://[redacted:email]/x", + "https://[redacted:email]/y", + ): + assert redact_sensitive_text(text) == text + + def test_email_in_path_not_redacted(self): + """An ``@`` in a path/query is not userinfo — the token class stops at + ``/``, so emails after the first slash are never treated as a credential.""" + for text in ( + "https://example.com/search?q=[redacted:email]", + "https://example.com/users/[redacted:email]/profile", + ): + assert redact_sensitive_text(text) == text + + def test_plain_url_unchanged(self): + text = "https://github.com/user/repo.git" + assert redact_sensitive_text(text) == text + + def test_long_bare_token_preserves_head_tail(self): + token = "[redacted:secret-assignment]" + "x" * 20 + "wxyz" + text = f"https://{token}@github.com/u/r.git" + result = redact_sensitive_text(text) + assert token not in result + assert "abcdef" in result # head preserved + assert "wxyz" in result # tail preserved + + class TestFormBodyRedaction: """Form-urlencoded body redaction (k=v&k=v with no other text)."""
-
2 PRs closed without merging
From the close comments: closed without comment 1 · other 1
Closed issues · 1
- Closed#6396[Bug]: Security
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.50: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- agent
- Merged PRs
- #54475
- Closed issues
- 1
- Attempts
- 2 PRs closed without merging
- Fix link checked
- 29 Jun 2026