Repository navigation
fix(credstore): return exact bytes from macOS Keychain reads - #78
Conversation
The read through /usr/bin/security used -w, which prints a secret holding any non-printable byte as bare hex. A PEM key, a multi-line value, a trailing newline or UTF-8 came back as a hex string with no error. -g marks the hex form with a 0x prefix, so the read now parses that and returns the stored bytes. Correct distribution.md section 2A: a stable designated requirement does not carry a grant across rebuilds on a binary without a TeamIdentifier, because the partition list gets a per-build cdhash entry. Twapp-Session: 59d912f3-982f-402e-8bd8-3e31269a28b0
piekstra-dev
left a comment
There was a problem hiding this comment.
Automated PR Review
Reviewed commit: fb45be6e4693
Profile: reviewer - Posting as: piekstra-dev
Summary
| Reviewer | Findings |
|---|---|
| documentation:drift | 0 |
| go:implementation-tests | 1 |
| privacy:captured-data | 0 |
| policies:conventions | 0 |
| security:code-auditor | 0 |
| go:implementation | 0 |
| documentation:docs | 0 |
| architecture:system | 1 |
go:implementation-tests (1 finding)
Minor - credstore/osbackend_keychain_stable_read_darwin_test.go:27
The
securityhelper closes over the parent test'stand callst.Fatalfon it. It is also invoked from insidet.Runsubtests (the add-generic-password calls at lines 61 and 70). FailNow and Fatalf must run on the goroutine of the test that owns thet. Calling the parent's Fatalf from a subtest goroutine marks the parent failed and then Goexits the subtest goroutine. That can surface as a confusing 'test executed panic(nil) or runtime.Goexit' failure instead of a clean subtest failure. Fix: have the helper take a*testing.T(security(t, args...)) and pass the subtest'st, or define the helper as a function withtas its first parameter. Keep the parent'stonly for the keychain creation and cleanup.
architecture:system (1 finding)
Major - credstore/osbackend_keychain_stable_read.go:56
This fix moves the library off
security -wbecause-wprints any secret holding a non-printable byte as bare hex, but the family's other Keychain reader still uses-w, and there the wrong value is written back and the original destroyed. The script is unchanged by this PR, so the defect is pre-existing; this PR is what establishes the root cause, anddocs/distribution.md§2A anddocs/working-with-secrets.mdboth point users at the script, the latter in the same paragraph that now says-wis not usable.Scenario: a user runs
scripts/repair-macos-keychain-credentials.sh --applywith a multi-line secret stored locally, such as the PEM private key this PR names for codereview-cli's GitHub App auth (add_cr_targetsenrolscrcredential refs).repair_itemcaptures the value with-w(line 266), so it holds the hex text. It hex-encodes that text again (line 270), deletes the original item (line 279), and recreates it with-X(line 291). The item now stores the ASCII hex text as its secret, and the script printsREPAIRED. Because this PR makes reads return exact bytes, every later read returns that hex text, sopem.Decodefails, with no copy of the original left to restore. The same$(...)capture would also strip trailing newlines from any value.I confirmed the capture step against real
<path>in a throwaway keychain: a four-byte secret ending in a newline came back from$(security ... -w)as its 8-character hex text, whichto_hexthen re-encodes into 16 hex digits. I did not run the script's--applypath end to end.Fix: read with
-gin the script too, and take the0xhex digits directly when present, otherwise hex-encode the quoted text, so-Xreceives the stored bytes. Abort before the delete on any unrecognized shape, mirroringparseKeychainPassword. Do this in this PR or as a tracked blocker forv0.5.1, because the docs now present the two paths as consistent.
Reviewer Coverage
Complete coverage details are retained in the local coverage.json artifact.
documentation:drift— complete (constrained); scope: 1 file; examples:credstore/osbackend_keychain_stable_read.go; body-inspected: 1 file; examples:credstore/osbackend_keychain_stable_read.go; skipped: none; context (not coverage): 2 files; examples:docs/distribution.md,docs/working-with-secrets.md; constraints: Compared against docs/distribution.md §2A, docs/working-with-secrets.md and the repair script by grep; did not execute the code orgo:implementation-tests— complete (broad); scope: 3 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go,credstore/osbackend_keychain_stable_read_test.go; body-inspected: 3 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go,credstore/osbackend_keychain_stable_read_test.go; skipped: none; constraints: Static review only; the darwin test against real was not run.privacy:captured-data— complete (constrained); scope: 5 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go,credstore/osbackend_keychain_stable_read_test.go,docs/distribution.md,docs/working-with-secrets.md; body-inspected: 5 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go,credstore/osbackend_keychain_stable_read_test.go,docs/distribution.md,docs/working-with-secrets.md; skipped: none; constraints: nonepolicies:conventions— complete (broad); scope: 3 files; examples:credstore/osbackend_keychain_stable_read.go,docs/distribution.md,docs/working-with-secrets.md; body-inspected: 3 files; examples:credstore/osbackend_keychain_stable_read.go,docs/distribution.md,docs/working-with-secrets.md; skipped: none; constraints: Convention review only; the parser's correctness against real security output was not executed. Could not verify the consumer-matrix release gate in docs/development.md beyond the PR description.security:code-auditor— complete (broad); scope: 2 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go; body-inspected: 2 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go; skipped: none; constraints: Only the first 1500 bytes of the darwin test file were read; the rest of that file was not read. The main source file was read in full. Static review only; tests were not executed and real output was not observed.go:implementation— complete (broad); scope: 1 file; examples:credstore/osbackend_keychain_stable_read.go; body-inspected: 1 file; examples:credstore/osbackend_keychain_stable_read.go; skipped: none; constraints: No code was executed; output shapes were taken from the PR description. Review limited to the assigned file body; test files (including the darwin real-security test) were listed but not read in detail.documentation:docs— complete (broad); scope: 2 files; examples:docs/distribution.md,docs/working-with-secrets.md; body-inspected: 2 files; examples:docs/distribution.md,docs/working-with-secrets.md; skipped: none; context (not coverage): 1 file; examples:credstore/osbackend_keychain_stable_read.go; constraints: Did not run the interactive Keychain prompt flow; ACL and partition-list behavior claims rest on the PR description and were not independently verified. Only the 60s timeout claim was checked against code (credstore/osbackend_keychain_stable_read.go).architecture:system— complete (broad); scope: 1 file; examples:credstore/osbackend_keychain_stable_read.go; body-inspected: 1 file; examples:credstore/osbackend_keychain_stable_read.go; skipped: none; context (not coverage): 7 files; examples:credstore/migration.go,credstore/osbackend_byteness.go,credstore/osbackend_keychain_stable_read_darwin_test.go,credstore/osbackend_keychain_stable_read_test.go,docs/distribution.md; 2 omitted; constraints: Not verified: output and exit code ofsecurity -gwhen the user denies the prompt or the keychain is locked, because that raises a dialog. An exit 0 with no password line would surface as the format error, not a denial; nothing observed shows this happens. Not verified: the 60-second timeout against a real pending prompt, or a real CLI upgrade, for the same reason. Ran the PR's real-security tests plus an added probe in a temp copy against a throwaway keychain: every byte value alone and betw…
0 PR discussion threads considered. 0 summarized; 0 resolved.
Completed in 4m 15s | $1.37 | claude-sonnet-5-5, claude-opus-5-5 | cr 0.10.363
| Field | Value |
|---|---|
| Model | claude-sonnet-5-5, claude-opus-5-5 |
| Reviewers | documentation:drift, go:implementation-tests, privacy:captured-data, policies:conventions, security:code-auditor, go:implementation, documentation:docs, architecture:system |
| Engine | claude_cli · claude-sonnet-5-5, claude-opus-5-5 |
| Reviewed by | cr · piekstra-dev |
| Duration | 4m 15s wall · 6m 27s compute |
| Cost | $1.37 |
| Tokens | 98 in / 23.9k out |
Per-workstream usage
orchestrator-selection— claude-sonnet-5-5- In: 6
- Out: 1.6k
- Cache read: 21.0k
- Cache create: 13.2k
- Cost: $0.07
- Duration: 14s
documentation:drift— claude-sonnet-5-5- In: 12
- Out: 1.7k
- Cache read: 75.6k
- Cache create: 16.3k
- Cost: $0.09
- Duration: 25s
go:implementation-tests— claude-sonnet-5-5- In: 8
- Out: 2.0k
- Cache read: 40.1k
- Cache create: 16.1k
- Cost: $0.09
- Duration: 22s
privacy:captured-data— claude-sonnet-5-5- In: 8
- Out: 1.1k
- Cache read: 43.7k
- Cache create: 16.9k
- Cost: $0.08
- Duration: 15s
policies:conventions— claude-sonnet-5-5- In: 10
- Out: 1.2k
- Cache read: 60.7k
- Cache create: 17.7k
- Cost: $0.09
- Duration: 17s
security:code-auditor— claude-sonnet-5-5- In: 10
- Out: 1.8k
- Cache read: 52.6k
- Cache create: 12.4k
- Cost: $0.07
- Duration: 19s
go:implementation— claude-sonnet-5-5- In: 8
- Out: 1.2k
- Cache read: 36.7k
- Cache create: 11.7k
- Cost: $0.06
- Duration: 20s
documentation:docs— claude-sonnet-5-5- In: 14
- Out: 1.7k
- Cache read: 84.6k
- Cache create: 13.0k
- Cost: $0.08
- Duration: 32s
architecture:system— claude-opus-5-5- In: 16
- Out: 11.0k
- Cache read: 203.3k
- Cache create: 42.4k
- Cost: $0.60
- Duration: 3m 22s
orchestrator-rollup— claude-sonnet-5-5- In: 6
- Out: 497
- Cache read: 66.5k
- Cache create: 4.6k
- Cost: $0.13
- Duration: 15s
repair_item read the secret with security -w, so a multi-line value came back as hex text. The script then deleted the item and recreated it holding that text, destroying the credential. It now reads with -g and refuses any unrecognized shape before the delete. Fail the darwin test on the subtest that owns the failure rather than on its parent. Twapp-Session: 59d912f3-982f-402e-8bd8-3e31269a28b0
piekstra-dev
left a comment
There was a problem hiding this comment.
Automated PR Review
Reviewed commit: 44f62c4fbd7d
Profile: reviewer - Posting as: piekstra-dev
Summary
| Reviewer | Findings |
|---|---|
| documentation:drift | 0 |
| go:implementation-tests | 0 |
| privacy:captured-data | 0 |
| policies:conventions | 0 |
| security:code-auditor | 0 |
| go:implementation | 0 |
| documentation:docs | 0 |
| architecture:system | 1 |
architecture:system (1 finding)
Blocking - credstore/osbackend_keychain_stable_read.go:59
44f62c4 fixes how
scripts/repair-macos-keychain-credentials.shreads a secret, so the read now matches this line, but the script's write path still destroys any secret longer than about 1.9 KB. The defect is in the script, which is outside this review's assigned file, so it is anchored here. The delete-then-recreate order is older than this PR, but this PR rewrites that path, describes it as keeping secret bytes, and tests it only with short values.Scenario: a user runs the script with
--applyon a long credential, such as an RSA-4096 PEM private key of about 3 KB, the same class of credential this PR cites for codereview-cli.repair_itemreads the secret and deletes the original item (security delete-generic-password). It then pipes oneadd-generic-password ... -X <hex>line intosecurity -i. Hex doubles the length, andsecurity -isplits lines at a limit near 4096 characters. The first fragment still parses, with its hex cut short, and is stored as the secret; the rest fails as an unknown command. The restore attempt builds an equally long line and hits the same limit, and its failure is ignored by|| true. The original is gone and the item holds a silently truncated secret.Measured with the test's
repair_itemdriver and real<path>, using synthetic values: 1600- and 1900-byte secrets were repaired. 2000-, 2100-, 3300- and 6000-byte secrets each ended with a 1983-byte stored value. The script printed therecreate failederror in the runs where that line was visible in my capture, and it prints no warning that the value was truncated. The cutoff shifts with the lengths of service, account, label and app path.The script's restore comment and §2A's description of it as a repair path imply the item survives a failure; here it does not.
TestRepairScriptKeepsSecretByteshas no long case.Fix: in
repair_item, build the full command before the delete and refuse with an error, leaving the item untouched, if the line exceeds a conservative limit such as 4000 characters. Also read the recreated item back and compare its hex to the original, reporting a mismatch loudly and without printing the value. Add a test that an over-limit secret is refused and the item is unchanged.Caution for that test: the driver appends the keychain path at the end of the line, so a line that reaches
security -iover the limit loses the path and writes to the user's default keychain. The refusal must happen before anysecurity -icall, or the test will pollute the real login keychain.
Reviewer Coverage
Complete coverage details are retained in the local coverage.json artifact.
documentation:drift— complete (constrained); scope: 2 files; examples:credstore/osbackend_keychain_stable_read.go,scripts/repair-macos-keychain-credentials.sh; body-inspected: 2 files; examples:credstore/osbackend_keychain_stable_read.go,scripts/repair-macos-keychain-credentials.sh; skipped: none; context (not coverage): 2 files; examples:docs/distribution.md,docs/working-with-secrets.md; constraints: Compared by reading and grep only; did not execute the script, the code, orgo:implementation-tests— complete (constrained); scope: 3 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go,credstore/osbackend_keychain_stable_read_test.go; body-inspected: 3 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go,credstore/osbackend_keychain_stable_read_test.go; skipped: none; constraints: Static review only; the darwin tests against real were not run. The repair shell script and docs are outside this agent's assigned scope; only the Go test that drives the script was reviewed.privacy:captured-data— complete (constrained); scope: 6 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go,credstore/osbackend_keychain_stable_read_test.go,docs/distribution.md,docs/working-with-secrets.md; 1 omitted; body-inspected: 6 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go,credstore/osbackend_keychain_stable_read_test.go,docs/distribution.md,docs/working-with-secrets.md; 1 omitted; skipped: none; constraints: nonepolicies:conventions— complete (constrained); scope: 3 files; examples:credstore/osbackend_keychain_stable_read.go,docs/distribution.md,docs/working-with-secrets.md; body-inspected: 3 files; examples:credstore/osbackend_keychain_stable_read.go,docs/distribution.md,docs/working-with-secrets.md; skipped: none; constraints: Convention review only; nothing was executed, including the parser and the repair script tests. The consumer-matrix release gate in docs/development.md was not verified beyond the PR description. The repair script and the darwin test files are outside the assigned scope and were not inspected as review targets.security:code-auditor— complete (constrained); scope: 2 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go; body-inspected: 2 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go; skipped: none; constraints: Static review only; tests and real is outside assignment scope and was not reviewed.go:implementation— complete (constrained); scope: 1 file; examples:credstore/osbackend_keychain_stable_read.go; body-inspected: 1 file; examples:credstore/osbackend_keychain_stable_read.go; skipped: none; constraints: No code was executed; output shapes were taken from the PR description. Review limited to the assigned file's diff and body; tests and the repair script were not reviewed.documentation:docs— complete (constrained); scope: 2 files; examples:docs/distribution.md,docs/working-with-secrets.md; body-inspected: 2 files; examples:docs/distribution.md,docs/working-with-secrets.md; skipped: none; constraints: Did not run the interactive Keychain prompt flow; ACL and partition-list behavior claims rest on the PR description and were not independently verified. The 60s timeout claim was checked against code only in the earlier review of the previous head; the docs diff is unchanged at this head.architecture:system— complete (constrained); scope: 1 file; examples:credstore/osbackend_keychain_stable_read.go; body-inspected: 1 file; examples:credstore/osbackend_keychain_stable_read.go; skipped: none; context (not coverage): 5 files; examples:credstore/osbackend_keychain_stable_read_darwin_test.go,credstore/osbackend_keychain_stable_read_test.go,docs/distribution.md,docs/working-with-secrets.md,scripts/repair-macos-keychain-credentials.sh; constraints: Not verified:security -goutput and exit code when a prompt is denied or the keychain is locked, the 60-second timeout against a real prompt, or a real CLI upgrade, because each needs a dialog or an installed release. The assigned Go file is unchanged since the previous review of fb45be6; this pass covered 44f62c4's script and test changes as context. The PR's repair, read and parser tests pass in a temp copy. The length probe used the test's driver, which appends a keychain path, so the exa…
0 PR discussion threads considered. 0 summarized; 0 resolved.
Completed in 3m 05s | $2.31 | claude-sonnet-5-5, claude-opus-5-5 | cr 0.10.363
| Field | Value |
|---|---|
| Model | claude-sonnet-5-5, claude-opus-5-5 |
| Reviewers | documentation:drift, go:implementation-tests, privacy:captured-data, policies:conventions, security:code-auditor, go:implementation, documentation:docs, architecture:system |
| Engine | claude_cli · claude-sonnet-5-5, claude-opus-5-5 |
| Reviewed by | cr · piekstra-dev |
| Duration | 3m 05s wall · 4m 37s compute |
| Cost | $2.31 |
| Tokens | 76 in / 17.3k out |
Per-workstream usage
documentation:drift— claude-sonnet-5-5- In: 8
- Out: 1.1k
- Cache read: 105.2k
- Cache create: 13.7k
- Cost: $0.17
- Duration: 21s
go:implementation-tests— claude-sonnet-5-5- In: 8
- Out: 1.1k
- Cache read: 103.1k
- Cache create: 13.2k
- Cost: $0.16
- Duration: 18s
privacy:captured-data— claude-sonnet-5-5- In: 8
- Out: 1.1k
- Cache read: 111.1k
- Cache create: 16.1k
- Cost: $0.17
- Duration: 18s
policies:conventions— claude-sonnet-5-5- In: 8
- Out: 1.1k
- Cache read: 105.7k
- Cache create: 10.7k
- Cost: $0.15
- Duration: 21s
security:code-auditor— claude-sonnet-5-5- In: 8
- Out: 1.1k
- Cache read: 88.7k
- Cache create: 13.2k
- Cost: $0.14
- Duration: 14s
go:implementation— claude-sonnet-5-5- In: 8
- Out: 864
- Cache read: 84.5k
- Cache create: 11.9k
- Cost: $0.13
- Duration: 27s
documentation:docs— claude-sonnet-5-5- In: 8
- Out: 1.0k
- Cache read: 89.3k
- Cache create: 11.9k
- Cost: $0.14
- Duration: 17s
architecture:system— claude-opus-5-5- In: 14
- Out: 9.4k
- Cache read: 416.9k
- Cache create: 26.6k
- Cost: $1.08
- Duration: 2m 02s
orchestrator-rollup— claude-sonnet-5-5- In: 6
- Out: 567
- Cache read: 79.9k
- Cache create: 4.3k
- Cost: $0.16
- Duration: 16s
security -i splits input lines near 4096 characters, so a long secret was truncated after the delete had removed the original. repair_item now builds the command first and refuses an over-long one with the item unchanged. It reads the recreated item back, compares it with the original, and restores on a mismatch. Recreated items also trust /usr/bin/security, which credstore reads through, so a repair no longer leaves one more prompt behind. Twapp-Session: 59d912f3-982f-402e-8bd8-3e31269a28b0
piekstra-dev
left a comment
There was a problem hiding this comment.
Automated PR Review
Reviewed commit: 024735dd3f2a
Profile: reviewer - Posting as: piekstra-dev
Summary
| Reviewer | Findings |
|---|---|
| documentation:drift | 0 |
| go:implementation-tests | 0 |
| privacy:captured-data | 0 |
| policies:conventions | 0 |
| security:code-auditor | 0 |
| go:implementation | 0 |
| documentation:docs | 0 |
| architecture:system | 2 |
architecture:system (2 findings)
Minor - credstore/osbackend_keychain_stable_read.go:3
The guard added to
runRepairItemincredstore/osbackend_keychain_stable_read_darwin_test.go(outside this review's assigned file, so anchored here) measures the line before the keychain path is appended, so it does not fully hold the settled thread's claim that the driver cannot write to the default keychain.Scenario: a developer or tool runs
go test ./credstoreon darwin with a longTMPDIR. This is not hypothetical: cr's own review subprocess sets aTMPDIRof 258 characters, andt.TempDir()adds more. The "long secret that fits one line" case builds a line of roughly 3840 characters, which passes the> 4000check. Adding the quoted keychain path takes it past the roughly 4096-character limit ofsecurity -i, so the path is cut. Theadd-generic-passwordthen no longer names the intended throwaway keychain.The consequence is likely a synthetic item in the user's real login keychain, plus a confusing test failure, which contradicts the test's stated guarantee of touching no real keychain. My evidence is from the previous review pass: a line cut inside the appended path left its item, with the full secret, in the login keychain. I did not reproduce this against the current driver, because doing so would write to the real keychain again.
Fix: check the final length in the driver, meaning
${#line}plus the keychain path and its quotes, and stop before callingsecurity -iif it is over the limit. Also create this keychain under a short fixed root, such asos.MkdirTemp("<path>", ...), so the 1800-byte case fits whateverTMPDIRis set to.
Minor - credstore/osbackend_keychain_stable_read.go:3
The new over-limit refusal in
scripts/repair-macos-keychain-credentials.sh(outside this review's assigned file, so anchored here) stops the whole batch, not just the one item. The script's usage text says only that such a secret "is refused and the item is left unchanged".Scenario: a user runs
--applywith a credential over about 1.9 KB, such as a long PEM under acrref, followed by other targets.repair_itemreturns 1 at the refusal.maincalls it bare inside its loop underset -euo pipefail, so the script exits there. Every later target is left unrepaired and theNotessection is never printed. Because the refusal depends only on secret length, every re-run stops at the same item, and the script has no option to skip it, so those later items can never be repaired by this script.I confirmed this by sourcing the script with
securityreplaced by a stub, so no keychain was involved: with three targets and an over-limit first item, only the first was processed, and the script ended right after its refusal message. Otherrepair_itemerrors already abort the same way, but they are transient, whereas this one is permanent for that credential.Fix: in
main, runrepair_item ... || failed=1, finish the loop, print the notes, then exit non-zero if any item failed. No item's state depends on an earlier one, so continuing is safe.
Reviewer Coverage
Complete coverage details are retained in the local coverage.json artifact.
documentation:drift— complete (constrained); scope: 2 files; examples:credstore/osbackend_keychain_stable_read.go,scripts/repair-macos-keychain-credentials.sh; body-inspected: 2 files; examples:credstore/osbackend_keychain_stable_read.go,scripts/repair-macos-keychain-credentials.sh; skipped: none; context (not coverage): 2 files; examples:docs/distribution.md,docs/working-with-secrets.md; constraints: Compared by reading and grep only; did not execute the script, the code, orgo:implementation-tests— complete (constrained); scope: 3 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go,credstore/osbackend_keychain_stable_read_test.go; body-inspected: 3 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go,credstore/osbackend_keychain_stable_read_test.go; skipped: none; constraints: Static review only; the darwin tests against real were not run. The repair shell script and docs are outside this agent's assigned scope; only the Go test that drives the script was reviewed.privacy:captured-data— complete (constrained); scope: 6 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go,credstore/osbackend_keychain_stable_read_test.go,docs/distribution.md,docs/working-with-secrets.md; 1 omitted; body-inspected: 6 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go,credstore/osbackend_keychain_stable_read_test.go,docs/distribution.md,docs/working-with-secrets.md; 1 omitted; skipped: none; constraints: nonepolicies:conventions— complete (constrained); scope: 3 files; examples:credstore/osbackend_keychain_stable_read.go,docs/distribution.md,docs/working-with-secrets.md; body-inspected: 3 files; examples:credstore/osbackend_keychain_stable_read.go,docs/distribution.md,docs/working-with-secrets.md; skipped: none; constraints: Convention review only; nothing was executed, including the parser and the repair script tests. The consumer-matrix release gate in docs/development.md was not verified beyond the PR description. The repair script and darwin test files are outside the assigned scope and were not inspected as review targets.security:code-auditor— complete (constrained); scope: 2 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go; body-inspected: 2 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go; skipped: none; constraints: Static review only; tests and real is outside assignment scope and was not reviewed; the darwin test's repair-script driver was read only as test code.go:implementation— complete (constrained); scope: 1 file; examples:credstore/osbackend_keychain_stable_read.go; body-inspected: 1 file; examples:credstore/osbackend_keychain_stable_read.go; skipped: none; constraints: No code was executed; output shapes were taken from the PR description. Review limited to the assigned Go file; tests and the repair script were not reviewed.documentation:docs— complete (constrained); scope: 2 files; examples:docs/distribution.md,docs/working-with-secrets.md; body-inspected: 2 files; examples:docs/distribution.md,docs/working-with-secrets.md; skipped: none; constraints: Did not run the interactive Keychain prompt flow; ACL and partition-list behavior claims rest on the PR description and were not independently verified. Docs files are unchanged since the previous reviewed head (44f62c4); the 60s timeout claim was checked against code in an earlier review.architecture:system— complete (constrained); scope: 1 file; examples:credstore/osbackend_keychain_stable_read.go; body-inspected: 1 file; examples:credstore/osbackend_keychain_stable_read.go; skipped: none; context (not coverage): 2 files; examples:credstore/osbackend_keychain_stable_read_darwin_test.go,scripts/repair-macos-keychain-credentials.sh; constraints: I did not run the PR's darwin tests or any realsecuritycommand this pass: with this environment's long TMPDIR the repair test could write to the real login keychain. The script's behaviour was checked with a stubbedsecurity, and the fix's real-keychain paths were read, not executed. Not verified:security -goutput and exit code when a prompt is denied or the keychain is locked, the 60-second timeout against a real prompt, or a real CLI upgrade, because each needs a dialog or an instal…
0 PR discussion threads considered. 0 summarized; 0 resolved.
Completed in 2m 14s | $3.25 | claude-sonnet-5-5, claude-opus-5-5 | cr 0.10.363
| Field | Value |
|---|---|
| Model | claude-sonnet-5-5, claude-opus-5-5 |
| Reviewers | documentation:drift, go:implementation-tests, privacy:captured-data, policies:conventions, security:code-auditor, go:implementation, documentation:docs, architecture:system |
| Engine | claude_cli · claude-sonnet-5-5, claude-opus-5-5 |
| Reviewed by | cr · piekstra-dev |
| Duration | 2m 14s wall · 3m 34s compute |
| Cost | $3.25 |
| Tokens | 70 in / 17.3k out |
Per-workstream usage
documentation:drift— claude-sonnet-5-5- In: 8
- Out: 1.3k
- Cache read: 157.7k
- Cache create: 11.3k
- Cost: $0.24
- Duration: 16s
go:implementation-tests— claude-sonnet-5-5- In: 8
- Out: 1.0k
- Cache read: 155.8k
- Cache create: 12.7k
- Cost: $0.24
- Duration: 17s
privacy:captured-data— claude-sonnet-5-5- In: 8
- Out: 1.1k
- Cache read: 173.5k
- Cache create: 13.8k
- Cost: $0.25
- Duration: 16s
policies:conventions— claude-sonnet-5-5- In: 8
- Out: 1.1k
- Cache read: 147.3k
- Cache create: 8.9k
- Cost: $0.21
- Duration: 17s
security:code-auditor— claude-sonnet-5-5- In: 8
- Out: 1.1k
- Cache read: 143.9k
- Cache create: 15.5k
- Cost: $0.23
- Duration: 12s
go:implementation— claude-sonnet-5-5- In: 8
- Out: 1.1k
- Cache read: 130.8k
- Cache create: 10.2k
- Cost: $0.19
- Duration: 18s
documentation:docs— claude-sonnet-5-5- In: 8
- Out: 1.0k
- Cache read: 135.1k
- Cache create: 9.9k
- Cost: $0.21
- Duration: 13s
architecture:system— claude-opus-5-5- In: 8
- Out: 9.0k
- Cache read: 317.6k
- Cache create: 18.5k
- Cost: $1.48
- Duration: 1m 33s
orchestrator-rollup— claude-sonnet-5-5- In: 6
- Out: 593
- Cache read: 93.1k
- Cache create: 4.6k
- Cost: $0.20
- Duration: 9s
A refused secret stayed refused on every run, so stopping at it left later items unrepairable. main now finishes the batch, prints the notes and exits non-zero if any item failed. The test driver counts the keychain path in its line limit and uses a short temp root, so a long TMPDIR cannot push a line past security -i and into the default keychain. Twapp-Session: 59d912f3-982f-402e-8bd8-3e31269a28b0
piekstra-dev
left a comment
There was a problem hiding this comment.
Automated PR Review
Reviewed commit: 97ddc0535855
Profile: reviewer - Posting as: piekstra-dev
Summary
| Reviewer | Findings |
|---|---|
| documentation:drift | 0 |
| go:implementation-tests | 0 |
| privacy:captured-data | 0 |
| policies:conventions | 0 |
| security:code-auditor | 0 |
| go:implementation | 0 |
| documentation:docs | 0 |
| architecture:system | 0 |
Reviewer Coverage
Complete coverage details are retained in the local coverage.json artifact.
documentation:drift— complete (constrained); scope: 2 files; examples:credstore/osbackend_keychain_stable_read.go,scripts/repair-macos-keychain-credentials.sh; body-inspected: 2 files; examples:credstore/osbackend_keychain_stable_read.go,scripts/repair-macos-keychain-credentials.sh; skipped: none; context (not coverage): 2 files; examples:docs/distribution.md,docs/working-with-secrets.md; constraints: Compared by reading and grep only; did not execute the script, the code, orgo:implementation-tests— complete (constrained); scope: 3 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go,credstore/osbackend_keychain_stable_read_test.go; body-inspected: 3 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go,credstore/osbackend_keychain_stable_read_test.go; skipped: none; constraints: Static review only; the darwin tests against real were not run. The repair shell script and docs are outside this agent's assigned scope; only the Go test that drives the script was reviewed.privacy:captured-data— complete (constrained); scope: 6 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go,credstore/osbackend_keychain_stable_read_test.go,docs/distribution.md,docs/working-with-secrets.md; 1 omitted; body-inspected: 6 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go,credstore/osbackend_keychain_stable_read_test.go,docs/distribution.md,docs/working-with-secrets.md; 1 omitted; skipped: none; constraints: nonepolicies:conventions— complete (constrained); scope: 3 files; examples:credstore/osbackend_keychain_stable_read.go,docs/distribution.md,docs/working-with-secrets.md; body-inspected: 3 files; examples:credstore/osbackend_keychain_stable_read.go,docs/distribution.md,docs/working-with-secrets.md; skipped: none; constraints: Convention review only; nothing was executed, including the parser and the repair script tests. The consumer-matrix release gate in docs/development.md was not verified beyond the PR description. The repair script and darwin test files are outside the assigned scope and were not inspected as review targets.security:code-auditor— complete (constrained); scope: 2 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go; body-inspected: 2 files; examples:credstore/osbackend_keychain_stable_read.go,credstore/osbackend_keychain_stable_read_darwin_test.go; skipped: none; constraints: Of the darwin test file, only lines 100-266 were read in this pass. Lines 1-99 changed in this commit (likely shortTempDir use) and were not re-read; I read an earlier version of them in a prior pass. Static review only; tests and real is unchanged since the previous head 024735d, which I reviewed; this pass confirmed that only via the commit diff stat, not a fresh read. scripts/repair-macos-keychain-credentials.sh is o…go:implementation— complete (constrained); scope: 1 file; examples:credstore/osbackend_keychain_stable_read.go; body-inspected: 1 file; examples:credstore/osbackend_keychain_stable_read.go; skipped: none; constraints: Manifest stats for the assigned Go file (74 additions, 11 deletions) match the earlier reviewed commits; I did not re-read the file at this head, relying on the earlier read. Tests and the repair script were not reviewed; no code was executed, and output shapes were taken from the PR description.documentation:docs— complete (constrained); scope: 2 files; examples:docs/distribution.md,docs/working-with-secrets.md; body-inspected: 2 files; examples:docs/distribution.md,docs/working-with-secrets.md; skipped: none; constraints: Did not run the interactive Keychain prompt flow; ACL and partition-list behavior claims rest on the PR description and were not independently verified. Docs files are unchanged since the previously reviewed head (024735d); the 60s timeout claim was checked against code in an earlier review.architecture:system— complete (constrained); scope: 1 file; examples:credstore/osbackend_keychain_stable_read.go; body-inspected: 1 file; examples:credstore/osbackend_keychain_stable_read.go; skipped: none; context (not coverage): 2 files; examples:credstore/osbackend_keychain_stable_read_darwin_test.go,scripts/repair-macos-keychain-credentials.sh; constraints: Not verified:security -goutput and exit code when a prompt is denied or the keychain is locked, the 60-second timeout against a real prompt, or a real CLI upgrade, because each needs a dialog or an installed release. Ran TestRepairScriptKeepsSecretBytes and the real-security round-trip test in a temp copy with the long TMPDIR still set: both pass. Attribute-only lookups before and after found no probe service items in the keychain search list. held no leftover test directories. The ass…
0 PR discussion threads considered. 0 summarized; 0 resolved.
Completed in 1m 14s | $4.07 | claude-sonnet-5-5, claude-opus-5-5 | cr 0.10.363
| Field | Value |
|---|---|
| Model | claude-sonnet-5-5, claude-opus-5-5 |
| Reviewers | documentation:drift, go:implementation-tests, privacy:captured-data, policies:conventions, security:code-auditor, go:implementation, documentation:docs, architecture:system |
| Engine | claude_cli · claude-sonnet-5-5, claude-opus-5-5 |
| Reviewed by | cr · piekstra-dev |
| Duration | 1m 14s wall · 3m 11s compute |
| Cost | $4.07 |
| Tokens | 70 in / 12.0k out |
Per-workstream usage
documentation:drift— claude-sonnet-5-5- In: 8
- Out: 1.2k
- Cache read: 203.5k
- Cache create: 11.4k
- Cost: $0.32
- Duration: 18s
go:implementation-tests— claude-sonnet-5-5- In: 8
- Out: 1.1k
- Cache read: 206.4k
- Cache create: 12.3k
- Cost: $0.32
- Duration: 23s
privacy:captured-data— claude-sonnet-5-5- In: 8
- Out: 1.0k
- Cache read: 228.5k
- Cache create: 13.3k
- Cost: $0.34
- Duration: 21s
policies:conventions— claude-sonnet-5-5- In: 8
- Out: 1.1k
- Cache read: 183.5k
- Cache create: 9.1k
- Cost: $0.28
- Duration: 14s
security:code-auditor— claude-sonnet-5-5- In: 10
- Out: 2.0k
- Cache read: 263.2k
- Cache create: 14.6k
- Cost: $0.34
- Duration: 32s
go:implementation— claude-sonnet-5-5- In: 6
- Out: 804
- Cache read: 124.5k
- Cache create: 10.1k
- Cost: $0.25
- Duration: 12s
documentation:docs— claude-sonnet-5-5- In: 8
- Out: 1.1k
- Cache read: 175.4k
- Cache create: 10.2k
- Cost: $0.28
- Duration: 14s
architecture:system— claude-opus-5-5- In: 8
- Out: 3.4k
- Cache read: 386.9k
- Cache create: 12.9k
- Cost: $1.73
- Duration: 46s
orchestrator-rollup— claude-sonnet-5-5- In: 6
- Out: 467
- Cache read: 105.8k
- Cache create: 3.4k
- Cost: $0.22
- Duration: 7s
|
Consumer matrix against merge commit f6def99, each repo at its current
The 3 codereview-cli failures are Tagging |
Problem
The stable read added in 86498ad runs
security find-generic-password -w.-wprints a secret holding any non-printable byte as bare hex, with no marker, so the read returned the hex string as the secret and reported success.Measured against real
/usr/bin/securityin a throwaway keychain:-wprints-gprintsabc123abc123password: "abc123"deadbeefdeadbeefpassword: "deadbeef"line1\nline26c696e65310a6c696e6532password: 0x6C696E65310A6C696E6532 "line1…tok\n746f6b0apassword: 0x746F6B0A "tok…päss70c3a47373password: 0x70C3A47373 "p\303\244ss"\xff\xfefffepassword: 0xFFFERows 2 and 6 show why
-woutput cannot be repaired after the fact: both print only hex digits, and only one is hex-encoded.Consumers feel this as a silently wrong credential: a PEM private key fails
pem.Decode, for example in codereview-cli's GitHub App auth. The existing test used a fake runner that echoed raw bytes, so it passed.scripts/repair-macos-keychain-credentials.shhad the same read and it destroyed data: it deleted the item and recreated it holding the hex text.docs/distribution.md§2A also claimed that a stable designated requirement makes a grant survive rebuilds. On a binary with no TeamIdentifier the partition list gets a per-buildcdhash:entry, so a native read prompts again.Change
-gand parses its stderr, which marks the hex form with0x. Parsing returns an error for any other shape, and that error never quotes the output./usr/bin/security.-gtoo, and refuses any unrecognized shape before the delete. It also refuses a secret too long for onesecurity -iline, which the old flow truncated after deleting the original. It reads each recreated item back and restores on a mismatch, and recreated items trust/usr/bin/security. Its top-level flow moved intomainso a test can source its functions./usr/bin/securityagainst a throwaway keychain in a temp directory: one pins the parser to actual output, the other runs the script's delete-and-recreate step and reads each secret back. They touch no real keychain and raise no dialog. The first found the0xFFFEshape, which has no display text.securityper item that does not yet trust it, then none across upgrades.Verification
make checkgo test -race, buildgo test -tags keyring_no1password,keyring_nopassage ./credstoreGOOS=windows go vet ./credstore,CGO_ENABLED=0 go vet ./credstore,shellcheckon the scriptStore.Geton synthetic login-keychain items created with-T /usr/bin/securityNot verified: the interactive prompt and Always Allow click, because a probe raises a dialog on the owner's screen, and a real upgrade of an installed CLI.
Release
This is a behaviour change to
credstore, so the tag waits for the consumer matrix indocs/development.md. The consumer test results against the merge commit go on this PR beforev0.5.1is cut.