Repository navigation
Detect a Bare secrets/ Directory From the Tracked Files That Name It - #3838
Conversation
The runtime-secrets check read only tracked paths, and a runtime-secrets directory is ignored by design, so a bare secrets/ never reached it. secrets.stems now also reads the tracked .gitignore, compose, and editor config files. A .gitignore line ignoring a directory named secrets, or a repo-relative secrets/<name> path, reports the bare directory. Each secret file such a path names that the tree lacks is judged by the env and secret file rule, and a .txt in a secrets directory now breaks it. A container mount point such as /run/secrets/<name> is left out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng It A bare secrets/ directory holding its own tracked .gitignore, .gitkeep, or .example template is now reported, and each template in it judged. A compose file is told from an editor config by its name, so one under .devcontainer resolves against its own directory. A path to a tracked file is no signal. A name stops at a colon, drops trailing dots, and is judged only when it is a plain file name. The devcontainer and shell workspace variables, nested and root devcontainer configs, and a backslashed editor path are read. A nested workspace resolves against its own root. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A reference into a secrets directory that tracks more than its own .gitignore, .gitkeep, README.md, and templates is no signal, since that directory is not ignored. That covers a directory, a glob, and a path with trailing prose punctuation, not only an exact tracked file. A named secret file is keyed on its resolved path, so two files naming one count it once and are both listed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
|
A recorded local strict-review pass covers head |
A secrets directory named with no file after it, such as a compose ./secrets:/run/secrets mount or a devcontainer bind, now reports the bare directory, while a bare word or a secrets: key does not. A path below a directory the tree lacks, such as a URL path or a cloud resource name, is no repo path. Only a secrets directory tracking more than its own files and templates, at any depth, counts as code, and that now also exempts its held templates and an anchored .gitignore line inside it. A relative editor path depends on a task's working directory, so it reports the directory but judges no file name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #3838 +/- ##
===========================================
+ Coverage 81.60% 83.29% +1.68%
===========================================
Files 26 29 +3
Lines 9070 9933 +863
===========================================
+ Hits 7402 8274 +872
+ Misses 1668 1659 -9
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…Folder
$PWD, ${PWD}, and $(pwd) name whatever directory a task or a compose
run starts in, so they are no repo path. A .gitignore line counts only
where it ignores a secrets folder itself or every file in it, and a
floating line counts only where no code directory named secrets sits
below it. A workflow file is no compose file whatever its name. The
walk over each path's secrets folders runs once, in secret_folders,
which now also returns the files a bare directory holds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Several moderate active findings in spec/audit.py leave valid secrets references undetected or misclassified, so the changes require fixes before approval.
5 open issues
Require repository path context for secret references · New Detect references to secrets directories without child paths · New Do not let nested templates hide invalid .secrets files · New Resolve editor paths relative to task working directories · New Apply code-directory exemptions to tracked signal files · New
What changed in this PR
This pull request expands the runtime-secrets audit to detect bare secrets/ directories and validate named secret files from tracked configuration files, including untracked files. It also enforces the .txt restriction for opaque credentials and reports unreadable signal files as undecided.
Changes:
- Parses
.gitignore, Compose, VS Code, and Dev Container configuration references. - Adds path resolution, deduplication, template, mount, and directory handling.
- Adds end-to-end and helper tests for the expanded audit behavior.
- Review findings remain in
spec/audit.py: five P1 moderate findings with 4 votes each concerning path-context parsing, directory-only references, nested.secretstemplates, code-directory exemptions, and editoroptions.cwd; plus one P2 moderate finding with 1 vote concerning structured-format parsing.
| File | Description |
|---|---|
tests/test_audit.py |
Adds fake GitHub responses and end-to-end coverage for tracked signals naming a bare secrets/ directory. |
tests/test_audit_helpers.py |
Adds parser, naming, path-resolution, deduplication, directory, template, and unreadable-input coverage. |
spec/audit.py |
Implements signal-file scanning, bare-directory detection, named secret validation, .txt enforcement, and unreadable-file reporting. Contains the six recorded review findings described above. |
Review effort: Lite
…ts Template A path into a secrets directory counts only where a ./, ../, or workspace variable marks it, or where it is a compose file: value, so a Vault path or prose naming secrets/ is no signal. A .gitignore line counts where every segment past the folder is a glob, so secrets/**/* reads as the folder. A file beside its own template is one of the directory's own files, so a committed credential no longer makes a bare directory read as code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
…the Move A compose path counts only on a line compose resolves on the host, a list item or a file, env_file, source, or context key, quoted or not. So a container-side environment or command value is no repo path, and a quoted file: value with no ./ is read. A .gitignore line is no signal once the tree or that file names a .secrets directory, since a repo that moved keeps the old line to guard existing checkouts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pace Variable Whether compose resolves a value on the host depends on its key, not on how its line starts. So a compose file is now parsed, and only its secret and config files, env files, bind sources, and build contexts are read. A command or healthcheck list item and a named volume are no repo path, and a long-syntax bind source and an env_file path entry are. An editor path counts only under a workspace variable, since any other depends on a launch's working directory. A compose file that does not parse is reported as undecided. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed by a Path A compose file carrying the !reset or !override tag now parses rather than reading as undecided. A path ending at a tracked file named secrets names no directory, and one ending at a tracked directory is not judged as a secret file. A .gitignore is no longer fetched once the tree carries a .secrets directory, since its lines would be discarded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
The compose read now goes through the existing _yaml helper, which
takes a loader, rather than a second parser of its own. A compose file
that does not parse, such as a Jinja template, is no signal rather than
a standing undecided finding nothing could clear. The compose name
pattern no longer matches composer.yml. An editor path rooted by
${workspaceRoot} or ${fileWorkspaceFolder} is read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A // comment line in an editor file is no live path, so a disabled mount or envFile no longer reports. An anchored .gitignore line now goes through secret_path_target like every other signal, so one below a directory the tree lacks, or naming a tracked file called secrets, is no signal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
A // comment after a code line in an editor file is now dropped like one opening a line, so a commented-out old secrets/ path no longer reports a bare directory. A // inside a URL, after a colon, is kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fleet keeps its VS Code settings in a root .code-workspace file, which can also carry launch and task entries, so the check now reads one. A compose bind source or build context and a devcontainer mount source may be directories, so each reports the secrets folder but no longer judges its last segment as a secret file name. Compose secret, config, and env files are still judged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A devcontainer config names paths mostly as mounts, run arguments, and commands, any of which may be a directory, in more forms than one pattern could tell apart. So a devcontainer path now reports the secrets folder but judges no file name, as a compose bind source does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
…tor Name
A .code-workspace file's ${workspaceFolder} now resolves against the
first folder its folders entry lists, and a file whose folder climbs
out of the repo is not read. An editor path may be a directory, as a
docker run -v bind in a task is, so its last segment is judged as a
secret file name only where it carries an extension, which a directory
rarely does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ne's Prefix A compose secret, config, or env file path ending at secrets names a file, so it no longer reports a bare directory. A floating .gitignore line with a glob before the folder, such as legacy/*/secrets/, now counts only where its literal leading directories exist, as the same line without the glob already did. The two path patterns share one folder fragment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
A floating .gitignore line such as deploy/**/secrets/ was silenced by a code secrets/ directory anywhere below the .gitignore, including one the line cannot match. The check now looks below the line's literal prefix, and below the .gitignore's directory only where the line has none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sentence period after an extensionless editor path passed the extension test, so a directory named in prose was judged as a secret file. The test now reads the name with its trailing dots dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
The secret_stem_findings docstring said only the templates directly in a bare secrets/ directory are judged. Every file directly in it other than its README.md, .gitignore, and .gitkeep is, a committed credential beside its template included, and the docstring now says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The case meant to show an anchored .gitignore line taking the same refusals as other paths also tracked a .secrets template, so the check skipped its .gitignore and the case proved nothing. Without the template the .gitignore is read, and dropping the refusals now fails the case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
A .gitignore was dropped as moved when any line named .secrets, a comment line included, so a note beside a live secrets/ line hid it. Comment lines are now skipped there. The .. guards and the **/ test, which other clauses already decided, are gone. A case now fails without each remaining guard: the moved drop, a tracked directory, the devcontainer no-name rule, a named volume, a plain path past the folder, an exempt file, the trailing-period strip, and the workflow exclusion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A held file's folder is never in holding, so testing it again was dead. A case now fails without the .gitignore negation guard, the reference pattern's segment-end lookahead, the plain file name check, the compose build context, and the compose configs section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
The cwd-relative case also carried a workflow file named like a compose file, but its text has no key compose reads, so the case passed with the workflow exclusion removed. The dedicated not-read test is what pins that exclusion, so the fixture and its label clause are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An editor path written with ${/} or ${pathSeparator} between its parts
is now read like one written with /. A case now fails without the
string form of a compose build, a negated .secrets line marking a
.gitignore as moved, and the template guard on an editor-named file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
…ranches ignored_secrets_folder's second value is true only for a line anchored with no glob before the folder, so it is now called literal, and the docstring says so. A compose value is matched as written, since a quoted one with spaces names another path. A case now fails without the long-syntax bind branch, the !override constructor, or the clause that keeps a tracked named file from counting twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The .lower() in the .txt test, the rstrip on the editor root, and the
normpath on a workspace root changed no result, since the name pattern
and secret_path_target already decide those cases. A case now fails
without ${pathSeparator}, the root .devcontainer.json pattern, the null
service guard, and a ? or [ glob before the folder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
SECRET_FOLDER already takes a leading ./ as a segment that normpath drops, so SECRET_PATH's own optional ./ changed no result. A case now fails without a floating line's fallback to its .gitignore's directory, the mapping branch of the compose tag constructor, a comment on an editor file's first line, and the strip of a padded .gitignore line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A floating line's prefix with no literal part is already its
.gitignore's directory, a compose name and an editor path never match
the same file, and an empty .gitignore line already names no secrets
folder, so each extra conditional is gone. A case now fails without
${containerWorkspaceFolder} and without the guard that reads no signal
file when no reader is given.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
…f Filing Issues (#3846) ## Summary Until now, filing an issue was always an acceptable way to dispose of a finding. backlog-burndown's file claim and its prose bound ("the next issue, filed") made filing the required route for a doc describing a group's own change. This PR closes those routes. A fix that can land in the pull request already open now lands there. Filing is kept for work that really needs its own lane. - **pr-review-conduct, outcome 4** (drive-pr carries it as an include): - An `introduced` finding is never filed. - An edge case on new code is fixed where its deployment can produce the input. Otherwise it is declined under Trust Boundaries. - A `pre-existing` correctness defect in a file the diff touches is fixed in this PR, within the same two-round budget. This uses the same file grain as the class sweep, and Merge Gate item 3 excepts these defects from the canonical tracker. - Only a `pre-existing` correctness defect **in code that runs**, elsewhere, is filed. - An edge case on new code follows Trust Boundaries' whole fix-or-decline test. - The open-ended "Any other deferral is filed" catch-all is gone. The only filings left are a design finding's untouched-file sites (one issue, per "Triaging findings") and a fix belonging to another repository. - **GOVERNANCE.md, Verification Discipline** (agent-conduct carries it as an include): the doc, manifest entry, or rule sentence describing changed behavior is a sibling the change put in disagreement. It is fixed in the change, not filed. - **backlog-burndown**: - Claims predict the docs that describe each fix. - A grown claim into an unclaimed file is granted at once. A grown claim into a held file is sequenced: the requester's PR waits for the holder's merge, and parks behind a holder that is stopped. A mutual wait is adjudicated as a conflict. A file on an unclaimed branch stops for the maintainer. - The prose bound admits units the change put in disagreement. - Growth into carried canonical content or a design change under a worker below `frontier` re-tiers the group, so that work never falls to a lower tier. - Hub workers read develop's copy of any skill develop has changed. This closes the stale-installed-skill gap behind the self-written reports on #3835, #3836, and #3838. - **local-strict-review**: the reviewer brief stops asking for wording, style, or consolidation remarks on text the diff did not write. Those were dropped anyway, so this only cuts output and disposal tokens. Maintainer decisions, 2026-11-02: - Lever 1: agreed. - Lever 2: file only code that runs. - In-file pre-existing defects: fix them in the PR. - Lever 3: rejected. ## Open `introduced` findings (loop stopped by the maintainer) There were twelve recorded local passes at the `frontier` tier, raising 10, 9, 8, 6, 3, 3, 5, 4, 4, 2, 6, and 3 findings. Nearly every finding was against text an earlier round had written. Of pass 11's six findings, 1 to 5 were fixed in d70a96e. Finding 6, which asked to revive the automatic re-tier, is declined: that hand-off drew new findings in every round, so it was replaced on purpose with the existing stop for the maintainer. By the maintainer's call, the last pass's findings get no further edit round. On 2026-11-02 the maintainer accepted both as known gaps (outcome 3): 1. `introduced`, backlog-burndown "Choosing the Worker's Model Tier": the tier-stop paragraph's "claim comment is extended first" does not repeat re-verify's limit to a file no branch holds. Read alone, it could extend a claim over a file another group holds. 2. `introduced`, pr-review-conduct outcome 4, last bullet: where the other repository's fix goes to the maintainer, no upstream issue exists for the local blocker record to name. ## Pre-existing findings, not filed - `pre-existing`, local-strict-review "The Carried-Content Sweep" with pr-review-conduct outcome 4: a sweep pass's findings are on units the sweep branch has not edited yet, so they route to the per-unit tracker rather than to a fix. - `pre-existing`, pr-review-conduct outcome 1 and "Triaging findings": the untouched-file sites of a design finding are routed two ways. One route files them as one issue, and the other drops them as a consolidation on untouched text. ## Replay of the 25 issues the run filed | Category | Issues | Under these rules | | --- | --- | --- | | Adjacent doc outside the claim | #3804, #3805, #3808, #3810, #3817, #3820, #3837 | Fixed in the originating PR through the predicted or granted claim. #3810 is a consolidation and #3817 is dead code, so each is fixed if in a touched file and otherwise dropped or listed | | Old defect next to the diff | #3813, #3821, #3824, #3825, #3833, #3834, #3841, #3844 | Fixed where it sits in a touched file or unit. Still filed only if it is code that runs elsewhere, which is about #3821, #3824, and #3841. #3833 keeps its canonical tracker | | Edge case past the done-definition | #3806, #3827, #3830, #3840, #3842 | `introduced`, so fixed in the PR or declined. #3830 is a contradiction the fix wrote and is never filed | | Decision | #3812, #3814, #3815, #3826 | Unchanged, still `decision` issues | Expected result for the same run: about 7 issues filed instead of 25. Closes on promotion: #3845 ## Verification - `scripts/build_dist.py --check`: generated distributions are current. - `prose_lint.py --diff origin/develop`: clean. - `spec/validate.py`: OK. - `repo_gate.py --check eol`: 0 issues. - Docker markdownlint and cspell: 0 issues. - local-strict-review: twelve passes recorded at the `frontier` tier. The receipt covers the pushed head. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…rge on a Running Hosted Review, and Gate Skill Description Length (#3843) Promotes the changes merged into `develop` since promotion #3797 to `main`, the end of an away-mode backlog burndown. - [#3803](#3803) exempts the Roslyn embedded-language marker (`// lang=json`, `language=`) from the prose gate's comment rules as a tool directive. - [#3807](#3807) has `local_review.py record` take `--report`, the file the reviewer wrote, bind the pass to its sha256, and derive the finding count from it. `status` gains `reportPath`. - [#3809](#3809) holds `pr_review.py merge` while a hosted review is still running on the head, and routes a declined Qodo finding to a dismiss request. - [#3811](#3811) dates an anchored `intentRef` by the section its anchor names, and lets a `driftNote` acknowledge a verified intent advisory. - [#3816](#3816) moves the null GraphQL node check into the shared `gh_graphql` transport as `NullNodeError`. - [#3818](#3818) explains the workspace-edit default in `GOVERNANCE.md` and names the aggregator's expression form. - [#3822](#3822) moves the abandon discard to the `backlog-burndown` orchestrator, aligns the abandoned-branch delete, and holds a grown agent-safety set. - [#3823](#3823) scopes the Husky prose gate to staged paths and adds `--skip-generated` so a named file inside a generated tree is skipped. - [#3828](#3828) reads Copilot's `Open findings:` label, drops the duplicate `null_node`, and matches `scripts/README.md` to the Merge Gate. - [#3829](#3829) bumps the two stale hub caller snippet pins and documents the fallback's mixed-pair refusal. - [#3831](#3831) keeps an inherited `GIT_DIR` from rerooting the prose gate's subdirectory git reads. - [#3832](#3832) gives `backlog-burndown` a disposition for a worker's standalone clone and keeps the agent-safety hold past a park. - [#3835](#3835) makes `carry.py check-sections` refuse what `apply-sections` would, and name an open fence. - [#3836](#3836) pins the reusable-workflow stubs by placeholder and ticks the PlexCleaner gates item. - [#3838](#3838) has the `runtime-secrets` audit dimension detect a bare `secrets/` directory from the tracked files that name it. - [#3839](#3839) gates skill descriptions at the 1024-character limit and trims every one over it. - [#3846](#3846) fixes an adjacent doc, an in-file pre-existing defect, and an edge case on new code in the open pull request instead of filing an issue for each, and keeps filing for code that runs elsewhere. - [#3847](#3847) refuses skill frontmatter that parses to a list or a scalar, so the description length gate cannot be bypassed. Fixes #3770 Fixes #3699 Fixes #3705 Fixes #3789 Fixes #1819 Fixes #727 Fixes #3710 Fixes #3706 Fixes #3623 Fixes #3687 Fixes #3617 Fixes #3757 Fixes #3790 Fixes #3786 Fixes #3785 Fixes #3772 Fixes #1657 Fixes #3443 Fixes #3824 Fixes #3817 Fixes #3805 Fixes #1666 Fixes #3700 Fixes #3821 Fixes #3771 Fixes #3794 Fixes #3825 Fixes #3690 Fixes #1472 Fixes #3813 Fixes #3719 Fixes #1592 Fixes #3845 🤖 Generated with [Claude Code](https://claude.com/claude-code)

Summary
The
runtime-secretsaudit dimension never reported a baresecrets/directory. A runtime-secrets directory is ignored by design, so it holds no tracked path, andsecret_stem_findingsjudged tracked paths only. The check now reads the tracked files that name such a directory instead.secrets.stemsreads each tracked.gitignoreand each compose or editor config file (compose*.yml,docker-compose*.yml,.vscode/*.json,.devcontainerconfigs). A compose file is read by its YAML keys, for the paths compose resolves on the host. A.gitignoreline ignoring a directory namedsecrets(secrets/,/secrets,**/secrets/,secrets/*), or a repo-relativesecrets/<name>path, reports the bare directory with each file naming it..secretsis never matched. A container mount point such as/run/secrets/<name>is out of scope, and so is a path that climbs out of the repository.secrets/or.secrets/, is judged by the env and secret file rule even though the tree lacks it. A compose path resolves against its own directory, and an editor path against the workspace root. A file the tree tracks, or whose.exampleit tracks, is judged once, from the tree..txtrule. A.txtextension on a file in a secrets directory now breaks the rule, per GOVERNANCE.md "Repo-Scoped Secrets" (a single opaque credential carries no extension). It applies to tracked and named files alike.secretsdirectory that tracks more than its own.gitignore,.gitkeep,README.md, templates, and files beside their templates is no signal, since that directory is not ignored. A bare directory's own tracked files report it../,../, or workspace variable marks it, or where it is a composefile:value. A URL path, a cloud resource name, a Vault path, or a shell$PWDis not one. A whole-directory mount counts. A relative editor path reports the directory but judges no file name, since it depends on a task's working directory..gitignoreline counts where it ignores the folder or its files by glob, and an anchored one inside a code directory does not.The issue's constructed case is the end-to-end test in
tests/test_audit.py. The parser and regex edges are intests/test_audit_helpers.py.spec/project-types.jsonalready states both the bare directory and the.txtfile as drift, so its assert text needs no change.Local review findings
Every
introducedfinding the local passes raised is fixed or filed, through266f99f6. The last pass, at266f99f6, raised one finding, deferred:spec/audit.py:543,introduced: a.gitignoreline with a character class in the folder segment, such as[Ss]ecrets/, is not matched. Deferred to Match a Capitalized Secrets Directory in the Runtime-Secrets Check #3842, which covers the same case-insensitive match.Declined findings
spec/project-types.json:175,introduced: a host-side home path such as~/.secrets/Api_Token.txtis not judged. Declined.AUDIT.mdsection 7's runtime-secrets bullet leaves host-side names to hand judgment: "The rest is judged by hand. ... A host-side name is judged where the repo's docs and scripts spell it out, never by reading a host."Pre-existing findings, filed
Run.read_textreads by branch name while the tree is listed at the head commit, so a push between the two turns a repository's result into one ERROR. This change adds reads through it.Follow-ups filed
Secrets/or.Secrets/directory, or a[Ss]ecrets/ignore line, is matched by no part of the check.dockerComposeFilelist, resolves against its first file. This check resolves each file against its own directory.AUDIT.mdsection 7's runtime-secrets bullet and thespec/checks.jsonsecrets.stemsdescription still describe the narrower check. Both are outside this change's files.Closes on promotion: #3719
🤖 Generated with Claude Code