From 2e6ae107273782e25dbb77e5bc05f47864c9cea1 Mon Sep 17 00:00:00 2001 From: David C Date: Wed, 21 Oct 2026 10:36:31 -0400 Subject: [PATCH 1/8] Document that the agent masks credential values in its config and log reads Refs HarperFast/harper#3097 Refs #674 Co-Authored-By: Claude Opus 5.5 --- reference/configuration/options.md | 25 ++++++++++++++++++++++++- release-notes/v5-lincoln/5.4.md | 4 ++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index d12128410..783002745 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -470,7 +470,30 @@ Whatever `componentsScope` and `configScope` say, every scope refuses: A key with neither a key file name nor PEM armor, such as a `.p12` bundle or raw DER, is not recognized outside the key directories. -The config file can still hold secrets that are not PEM keys, such as a model `apiKey` or storage credentials, and the agent reads it. Setting `configScope` to `rootPath` also restores read access to the raw database files and backups. +Setting `configScope` to `rootPath` also restores read access to the raw database files and backups. + +### Masked credential values + + + +The config file can hold plaintext credentials: a model `apiKey` passed through `HARPER_SET_CONFIG` is written there. Logs hold whatever a component logged. When the agent reads the `logs` or `config` scope, the value of every credential-named key comes back as `*`s, one per character: + +```yaml +models: + generative: + default: + apiKey: ************** + model: gpt-5 +``` + +- **Which keys.** A key that ends in `key`, `token`, `cookie`, or `authorization`, or contains `secret`, `password`, `passwd`, `passphrase`, or `credential`, and `accessKeyId` in any spelling. The whole key must match, so settings such as `maxTokens` and `operationTokenTimeout` are shown. A value that is exactly an environment reference, such as `${OPENAI_API_KEY}`, is shown as written. +- **Which forms.** `key: value` and `key=value`, with the key quoted or not, anywhere in a line: YAML, JSON, `Authorization: Bearer ...` headers, and `?api_key=...` query strings. A quoted value is masked up to its closing quote, an unquoted one to the end of the line. A YAML value that continues on more-indented lines, such as a long folded value or a `|` block, is masked on every line. +- **Positional reads.** The mask keeps the text's length, so a `read_file` `offset` into a value, the later parts of a long line, and the end of a line that `tail_file` returns are masked too. `size` and `nextOffset` still count the file's bytes, so a masked value's length is visible; its content is not. +- **Searches.** `grep_files` searches the masked text, so a pattern that matches only a credential's value finds nothing. + +The agent's `read_log` tool is masked the same way, in every text field of each entry. An entry holding a PEM private key is replaced by a refusal. The tool refuses `read_log`'s `filter` argument, because `filter` matches the unmasked log. `read_log` called through the operations API or an MCP client returns the log unmasked. + +The `components` scope is not masked, so a file the agent reads and writes back is unchanged. A credential that is not the value of a credential-named key, such as a password inside a URL, is not masked. ### Restricting `http_fetch` diff --git a/release-notes/v5-lincoln/5.4.md b/release-notes/v5-lincoln/5.4.md index ad0d66d61..fa79c1e6e 100644 --- a/release-notes/v5-lincoln/5.4.md +++ b/release-notes/v5-lincoln/5.4.md @@ -38,6 +38,10 @@ A model reply that did not finish now ends the agent session `error`, with the r The built-in agent's read-only `config` filesystem scope used to be the directory holding `harper-config.yaml`. On a default install that is `rootPath`, so the agent could read `keys/`, `ssh/`, and the raw database files. It is now the config file alone. The new `agent.configScope` replaces it at startup with another file or directory, which keeps the config file readable only if it contains it. In every scope, the agent's filesystem tools now refuse to read Harper's key directories (`/keys` and `/ssh`), files named `*.pem`, `*.key`, or `.jwtPass`, and text holding a PEM private key, and they refuse to write into the key directories. `set_agent_config` now rejects `configScope` and `componentsScope` with a `400`, as it already did `httpFetch`. An agent that read other files through `config` needs `configScope` set to a directory that holds them. See [Filesystem scopes](/reference/v5/configuration/options#filesystem-scopes) ([harper#3041](https://github.com/HarperFast/harper/issues/3041)). +### Credential Values Are Masked in the Agent's Config and Log Reads + +The config file can hold plaintext credentials, such as a model `apiKey` passed through `HARPER_SET_CONFIG`, and the agent's `config` scope returned them, as did its `logs` scope and `read_log` for anything a component logged. The agent's reads of the `logs` and `config` scopes, and its `read_log` results, now show the value of every credential-named key (`apiKey`, `password`, `token`, `secret`, and similar) as `*`s, including a value that continues on more-indented YAML lines and a value a positional read starts in the middle of. `grep_files` searches the masked text in those scopes, and the agent's `read_log` refuses `filter`, which matches the unmasked log. The `components` scope, and `read_log` called through the operations API or an MCP client, are unchanged. Sessions recorded before this release keep whatever they already read, so rotate any credential an agent session may have read. See [Masked credential values](/reference/v5/configuration/options#masked-credential-values) ([harper#3097](https://github.com/HarperFast/harper/issues/3097)). + ### Capped and Paged Agent Tool Results The built-in agent added every tool result to its conversation in full, so one large result, such as a log read whole, could fill the model's context window and leave the session unusable. A tool result is now cut to `agent.maxToolResultBytes` (default `65536`) before it is stored, and `read_file` pages through a file by line, with no size limit, returning a cursor (`nextLine`, `nextOffset`) to continue from. When a provider still rejects a request as too long for the model, the agent cuts the most recent tool results to 2 KiB and retries once, and prompting a session left unusable by an earlier version usually recovers it the same way. The agent also stops sending its system prompt twice per request. See [When a conversation outgrows the model's context window](/reference/v5/operations-api/operations#when-a-conversation-outgrows-the-models-context-window). From b12f2183309f9925da84e58c463f6dece4dd8895 Mon Sep 17 00:00:00 2001 From: David C Date: Wed, 21 Oct 2026 10:59:07 -0400 Subject: [PATCH 2/8] Say that a quoted value is masked to its closing quote on a later line, and a key's items too Refs HarperFast/harper#3097 Refs #674 Co-Authored-By: Claude Opus 5.5 --- reference/configuration/options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 783002745..0a585dbb2 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -487,7 +487,7 @@ models: ``` - **Which keys.** A key that ends in `key`, `token`, `cookie`, or `authorization`, or contains `secret`, `password`, `passwd`, `passphrase`, or `credential`, and `accessKeyId` in any spelling. The whole key must match, so settings such as `maxTokens` and `operationTokenTimeout` are shown. A value that is exactly an environment reference, such as `${OPENAI_API_KEY}`, is shown as written. -- **Which forms.** `key: value` and `key=value`, with the key quoted or not, anywhere in a line: YAML, JSON, `Authorization: Bearer ...` headers, and `?api_key=...` query strings. A quoted value is masked up to its closing quote, an unquoted one to the end of the line. A YAML value that continues on more-indented lines, such as a long folded value or a `|` block, is masked on every line. +- **Which forms.** `key: value` and `key=value`, with the key quoted or not, anywhere in a line: YAML, JSON, `Authorization: Bearer ...` headers, and `?api_key=...` query strings. A quoted value is masked up to its closing quote, even on a later line, and an unquoted one to the end of the line. A YAML value that continues on more-indented lines, such as a long folded value or a `|` block, is masked on every line, as are the `- ` items under a key with no value of its own. - **Positional reads.** The mask keeps the text's length, so a `read_file` `offset` into a value, the later parts of a long line, and the end of a line that `tail_file` returns are masked too. `size` and `nextOffset` still count the file's bytes, so a masked value's length is visible; its content is not. - **Searches.** `grep_files` searches the masked text, so a pattern that matches only a credential's value finds nothing. From 8db9bcf94a43311fe63cf8706d85304386d76562 Mon Sep 17 00:00:00 2001 From: David C Date: Wed, 21 Oct 2026 11:26:23 -0400 Subject: [PATCH 3/8] Say that JSON logged inside a JSON string is masked too Refs HarperFast/harper#3097 Refs #674 Co-Authored-By: Claude Opus 5.5 --- reference/configuration/options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 0a585dbb2..9d9d93532 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -487,7 +487,7 @@ models: ``` - **Which keys.** A key that ends in `key`, `token`, `cookie`, or `authorization`, or contains `secret`, `password`, `passwd`, `passphrase`, or `credential`, and `accessKeyId` in any spelling. The whole key must match, so settings such as `maxTokens` and `operationTokenTimeout` are shown. A value that is exactly an environment reference, such as `${OPENAI_API_KEY}`, is shown as written. -- **Which forms.** `key: value` and `key=value`, with the key quoted or not, anywhere in a line: YAML, JSON, `Authorization: Bearer ...` headers, and `?api_key=...` query strings. A quoted value is masked up to its closing quote, even on a later line, and an unquoted one to the end of the line. A YAML value that continues on more-indented lines, such as a long folded value or a `|` block, is masked on every line, as are the `- ` items under a key with no value of its own. +- **Which forms.** `key: value` and `key=value`, with the key quoted or not, anywhere in a line: YAML, JSON (including JSON logged inside a JSON string), `Authorization: Bearer ...` headers, and `?api_key=...` query strings. A quoted value is masked up to its closing quote, even on a later line, and an unquoted one to the end of the line. A YAML value that continues on more-indented lines, such as a long folded value or a `|` block, is masked on every line, as are the `- ` items under a key with no value of its own. - **Positional reads.** The mask keeps the text's length, so a `read_file` `offset` into a value, the later parts of a long line, and the end of a line that `tail_file` returns are masked too. `size` and `nextOffset` still count the file's bytes, so a masked value's length is visible; its content is not. - **Searches.** `grep_files` searches the masked text, so a pattern that matches only a credential's value finds nothing. From ddac0db1839766ebff453c752d1702f2356b330c Mon Sep 17 00:00:00 2001 From: David C Date: Wed, 21 Oct 2026 12:15:13 -0400 Subject: [PATCH 4/8] Say that config-scope files are also parsed as YAML for masking Refs HarperFast/harper#3097 Refs #674 Co-Authored-By: Claude Opus 5.5 --- reference/configuration/options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 9d9d93532..6b9f92026 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -487,7 +487,7 @@ models: ``` - **Which keys.** A key that ends in `key`, `token`, `cookie`, or `authorization`, or contains `secret`, `password`, `passwd`, `passphrase`, or `credential`, and `accessKeyId` in any spelling. The whole key must match, so settings such as `maxTokens` and `operationTokenTimeout` are shown. A value that is exactly an environment reference, such as `${OPENAI_API_KEY}`, is shown as written. -- **Which forms.** `key: value` and `key=value`, with the key quoted or not, anywhere in a line: YAML, JSON (including JSON logged inside a JSON string), `Authorization: Bearer ...` headers, and `?api_key=...` query strings. A quoted value is masked up to its closing quote, even on a later line, and an unquoted one to the end of the line. A YAML value that continues on more-indented lines, such as a long folded value or a `|` block, is masked on every line, as are the `- ` items under a key with no value of its own. +- **Which forms.** `key: value` and `key=value`, with the key quoted or not, anywhere in a line: YAML, JSON (including JSON logged inside a JSON string), `Authorization: Bearer ...` headers, and `?api_key=...` query strings. A quoted value is masked up to its closing quote, even on a later line, and an unquoted one to the end of the line. A YAML value that continues on more-indented lines, such as a long folded value or a `|` block, is masked on every line, as are the `- ` items under a key with no value of its own. A config-scope file of up to 5 MiB is also parsed as YAML, so a key written with escapes, such as `"api\u004Bey"`, and a value reached through an alias are masked too. - **Positional reads.** The mask keeps the text's length, so a `read_file` `offset` into a value, the later parts of a long line, and the end of a line that `tail_file` returns are masked too. `size` and `nextOffset` still count the file's bytes, so a masked value's length is visible; its content is not. - **Searches.** `grep_files` searches the masked text, so a pattern that matches only a credential's value finds nothing. From 67121ad743b7e346381de5effda0ac3a857e6173 Mon Sep 17 00:00:00 2001 From: David C Date: Wed, 21 Oct 2026 12:25:15 -0400 Subject: [PATCH 5/8] Say that a config file that does not parse shows only its keys, and a large one is refused Refs HarperFast/harper#3097 Refs #674 Co-Authored-By: Claude Opus 5.5 --- reference/configuration/options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 6b9f92026..ad3715266 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -487,7 +487,7 @@ models: ``` - **Which keys.** A key that ends in `key`, `token`, `cookie`, or `authorization`, or contains `secret`, `password`, `passwd`, `passphrase`, or `credential`, and `accessKeyId` in any spelling. The whole key must match, so settings such as `maxTokens` and `operationTokenTimeout` are shown. A value that is exactly an environment reference, such as `${OPENAI_API_KEY}`, is shown as written. -- **Which forms.** `key: value` and `key=value`, with the key quoted or not, anywhere in a line: YAML, JSON (including JSON logged inside a JSON string), `Authorization: Bearer ...` headers, and `?api_key=...` query strings. A quoted value is masked up to its closing quote, even on a later line, and an unquoted one to the end of the line. A YAML value that continues on more-indented lines, such as a long folded value or a `|` block, is masked on every line, as are the `- ` items under a key with no value of its own. A config-scope file of up to 5 MiB is also parsed as YAML, so a key written with escapes, such as `"api\u004Bey"`, and a value reached through an alias are masked too. +- **Which forms.** `key: value` and `key=value`, with the key quoted or not, anywhere in a line: YAML, JSON (including JSON logged inside a JSON string), `Authorization: Bearer ...` headers, and `?api_key=...` query strings. A quoted value is masked up to its closing quote, even on a later line, and an unquoted one to the end of the line. A YAML value that continues on more-indented lines, such as a long folded value or a `|` block, is masked on every line, as are the `- ` items under a key with no value of its own. In the `config` scope each file is also parsed as YAML, so a key written with escapes, such as `"api\u004Bey"`, and a value reached through an alias are masked too. A config file that does not parse shows only its keys, with every value masked, and one over 256 KiB or not UTF-8 is refused. - **Positional reads.** The mask keeps the text's length, so a `read_file` `offset` into a value, the later parts of a long line, and the end of a line that `tail_file` returns are masked too. `size` and `nextOffset` still count the file's bytes, so a masked value's length is visible; its content is not. - **Searches.** `grep_files` searches the masked text, so a pattern that matches only a credential's value finds nothing. From c77edfd7c561d6ec1162cf092daa81a7e8b5d62a Mon Sep 17 00:00:00 2001 From: David C Date: Wed, 21 Oct 2026 13:12:22 -0400 Subject: [PATCH 6/8] Say that a widened configScope still refuses database files and backups, and how many stars a value gets Refs HarperFast/harper#3097 Refs #674 Co-Authored-By: Claude Opus 5.5 --- reference/configuration/options.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index ad3715266..16e4ad311 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -470,13 +470,13 @@ Whatever `componentsScope` and `configScope` say, every scope refuses: A key with neither a key file name nor PEM armor, such as a `.p12` bundle or raw DER, is not recognized outside the key directories. -Setting `configScope` to `rootPath` also restores read access to the raw database files and backups. +Setting `configScope` to `rootPath` also exposes the rest of `rootPath`, though a file over 256 KiB or not UTF-8, such as a database file or a backup, is refused (see [Masked credential values](#masked-credential-values)). ### Masked credential values -The config file can hold plaintext credentials: a model `apiKey` passed through `HARPER_SET_CONFIG` is written there. Logs hold whatever a component logged. When the agent reads the `logs` or `config` scope, the value of every credential-named key comes back as `*`s, one per character: +The config file can hold plaintext credentials: a model `apiKey` passed through `HARPER_SET_CONFIG` is written there. Logs hold whatever a component logged. When the agent reads the `logs` or `config` scope, the value of every credential-named key comes back as `*`s: ```yaml models: @@ -488,7 +488,7 @@ models: - **Which keys.** A key that ends in `key`, `token`, `cookie`, or `authorization`, or contains `secret`, `password`, `passwd`, `passphrase`, or `credential`, and `accessKeyId` in any spelling. The whole key must match, so settings such as `maxTokens` and `operationTokenTimeout` are shown. A value that is exactly an environment reference, such as `${OPENAI_API_KEY}`, is shown as written. - **Which forms.** `key: value` and `key=value`, with the key quoted or not, anywhere in a line: YAML, JSON (including JSON logged inside a JSON string), `Authorization: Bearer ...` headers, and `?api_key=...` query strings. A quoted value is masked up to its closing quote, even on a later line, and an unquoted one to the end of the line. A YAML value that continues on more-indented lines, such as a long folded value or a `|` block, is masked on every line, as are the `- ` items under a key with no value of its own. In the `config` scope each file is also parsed as YAML, so a key written with escapes, such as `"api\u004Bey"`, and a value reached through an alias are masked too. A config file that does not parse shows only its keys, with every value masked, and one over 256 KiB or not UTF-8 is refused. -- **Positional reads.** The mask keeps the text's length, so a `read_file` `offset` into a value, the later parts of a long line, and the end of a line that `tail_file` returns are masked too. `size` and `nextOffset` still count the file's bytes, so a masked value's length is visible; its content is not. +- **Positional reads.** The mask keeps each value in place, one `*` per byte in a config file and per character in a log, so a `read_file` `offset` into a value, the later parts of a long line, and the end of a line that `tail_file` returns are masked too. `size` and `nextOffset` still count the file's bytes, so a masked value's length is visible; its content is not. - **Searches.** `grep_files` searches the masked text, so a pattern that matches only a credential's value finds nothing. The agent's `read_log` tool is masked the same way, in every text field of each entry. An entry holding a PEM private key is replaced by a refusal. The tool refuses `read_log`'s `filter` argument, because `filter` matches the unmasked log. `read_log` called through the operations API or an MCP client returns the log unmasked. From fc1da6ab8f970ed975b7f0f9a896f6d0c555c99c Mon Sep 17 00:00:00 2001 From: David C Date: Wed, 21 Oct 2026 13:14:33 -0400 Subject: [PATCH 7/8] Drop the per-character star count for logs Refs HarperFast/harper#3097 Refs #674 Co-Authored-By: Claude Opus 5.5 --- reference/configuration/options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 16e4ad311..946622643 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -488,7 +488,7 @@ models: - **Which keys.** A key that ends in `key`, `token`, `cookie`, or `authorization`, or contains `secret`, `password`, `passwd`, `passphrase`, or `credential`, and `accessKeyId` in any spelling. The whole key must match, so settings such as `maxTokens` and `operationTokenTimeout` are shown. A value that is exactly an environment reference, such as `${OPENAI_API_KEY}`, is shown as written. - **Which forms.** `key: value` and `key=value`, with the key quoted or not, anywhere in a line: YAML, JSON (including JSON logged inside a JSON string), `Authorization: Bearer ...` headers, and `?api_key=...` query strings. A quoted value is masked up to its closing quote, even on a later line, and an unquoted one to the end of the line. A YAML value that continues on more-indented lines, such as a long folded value or a `|` block, is masked on every line, as are the `- ` items under a key with no value of its own. In the `config` scope each file is also parsed as YAML, so a key written with escapes, such as `"api\u004Bey"`, and a value reached through an alias are masked too. A config file that does not parse shows only its keys, with every value masked, and one over 256 KiB or not UTF-8 is refused. -- **Positional reads.** The mask keeps each value in place, one `*` per byte in a config file and per character in a log, so a `read_file` `offset` into a value, the later parts of a long line, and the end of a line that `tail_file` returns are masked too. `size` and `nextOffset` still count the file's bytes, so a masked value's length is visible; its content is not. +- **Positional reads.** The mask keeps each value in place (in a config file, one `*` per byte), so a `read_file` `offset` into a value, the later parts of a long line, and the end of a line that `tail_file` returns are masked too. `size` and `nextOffset` still count the file's bytes, so a masked value's length is visible; its content is not. - **Searches.** `grep_files` searches the masked text, so a pattern that matches only a credential's value finds nothing. The agent's `read_log` tool is masked the same way, in every text field of each entry. An entry holding a PEM private key is replaced by a refusal. The tool refuses `read_log`'s `filter` argument, because `filter` matches the unmasked log. `read_log` called through the operations API or an MCP client returns the log unmasked. From ffb2f204731e7d8f5e27871372f9fc63d86314b8 Mon Sep 17 00:00:00 2001 From: David C Date: Wed, 21 Oct 2026 16:38:38 -0400 Subject: [PATCH 8/8] Mark the masking section as a change, and give the refusal its own sentence Refs HarperFast/harper#3097 Refs #674 Co-Authored-By: Claude Opus 5.5 --- reference/configuration/options.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 946622643..961f10592 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -474,7 +474,7 @@ Setting `configScope` to `rootPath` also exposes the rest of `rootPath`, though ### Masked credential values - + The config file can hold plaintext credentials: a model `apiKey` passed through `HARPER_SET_CONFIG` is written there. Logs hold whatever a component logged. When the agent reads the `logs` or `config` scope, the value of every credential-named key comes back as `*`s: @@ -487,7 +487,7 @@ models: ``` - **Which keys.** A key that ends in `key`, `token`, `cookie`, or `authorization`, or contains `secret`, `password`, `passwd`, `passphrase`, or `credential`, and `accessKeyId` in any spelling. The whole key must match, so settings such as `maxTokens` and `operationTokenTimeout` are shown. A value that is exactly an environment reference, such as `${OPENAI_API_KEY}`, is shown as written. -- **Which forms.** `key: value` and `key=value`, with the key quoted or not, anywhere in a line: YAML, JSON (including JSON logged inside a JSON string), `Authorization: Bearer ...` headers, and `?api_key=...` query strings. A quoted value is masked up to its closing quote, even on a later line, and an unquoted one to the end of the line. A YAML value that continues on more-indented lines, such as a long folded value or a `|` block, is masked on every line, as are the `- ` items under a key with no value of its own. In the `config` scope each file is also parsed as YAML, so a key written with escapes, such as `"api\u004Bey"`, and a value reached through an alias are masked too. A config file that does not parse shows only its keys, with every value masked, and one over 256 KiB or not UTF-8 is refused. +- **Which forms.** `key: value` and `key=value`, with the key quoted or not, anywhere in a line: YAML, JSON (including JSON logged inside a JSON string), `Authorization: Bearer ...` headers, and `?api_key=...` query strings. A quoted value is masked up to its closing quote, even on a later line, and an unquoted one to the end of the line. A YAML value that continues on more-indented lines, such as a long folded value or a `|` block, is masked on every line, as are the `- ` items under a key with no value of its own. In the `config` scope each file is also parsed as YAML, so a key written with escapes, such as `"api\u004Bey"`, and a value reached through an alias are masked too. A config file that does not parse shows only its keys, with every value masked. A config file over 256 KiB or not UTF-8 is refused. - **Positional reads.** The mask keeps each value in place (in a config file, one `*` per byte), so a `read_file` `offset` into a value, the later parts of a long line, and the end of a line that `tail_file` returns are masked too. `size` and `nextOffset` still count the file's bytes, so a masked value's length is visible; its content is not. - **Searches.** `grep_files` searches the masked text, so a pattern that matches only a credential's value finds nothing.