security(history): scrub admin:admin123 from .claude/skills/transcribe/SKILL.md git history #460
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
gitleaks detect --source .reports the credentialadmin:admin123committed to git history at line 96 of.claude/skills/transcribe/SKILL.md, in commits3d3d4b86andfc27043d(both 2026-04-14):The same string appears in 6 worktrees under
.worktrees/and.claude/worktrees/(working-tree only — those won't propagate when push-cleaned).This is the same credential still hardcoded in
application.yaml:67(#83). Even after #83 lands, the valueadmin123should be removed from the documentation history because:Approach
Two-step:
1. Replace the literal in current docs
Change every occurrence of
admin:admin123in.claude/skills/transcribe/SKILL.mdand any other tracked file toadmin:$ADMIN_PASSwith a one-line note:Commit and push.
2. Scrub from history
Pick
git filter-repo(preferred, faster, sanctioned) overbfg:After force-push to
main:git fetch --all && git reset --hard origin/main.3d3d4b86,fc27043d) become rewritten commits with new SHAs.3. Rotate the credential anyway
Even after scrubbing, treat
admin123as compromised forever (it was public). Once #83 lands, set a strongAPP_ADMIN_PASSWORDand rotate any account that ever used the default.Critical files
.claude/skills/transcribe/SKILL.md(line 96 + any other occurrences)expressions.txt(one-shot, not committed)Verification
gitleaks detect --source .after the scrub returns 0 secrets in history.gitleaks dir .working-tree scan returns 0 secrets (after worktrees are also cleaned).git log -p .claude/skills/transcribe/SKILL.md | grep -c admin:admin123→ 0.$ADMIN_PASSwith a comment.Acceptance criteria
admin:admin123in any tracked file (working tree).admin:admin123in any commit reachable frommainor any branch..worktrees/either deleted or refreshed.Effort
S — 1 hour for the scrub. The rotation depends on #83.
Risk if not addressed
The default password value is permanently public in git reflog. Any future deployment misconfiguration that re-enables the default (per #83) immediately exposes a known credential.
Tracked in audit doc as A.3 of
docs/audits/2026-05-07-pre-prod-architectural-review.md.🔐 Nora "NullX" Steiner — Application Security Engineer
Observations
Two distinct problems, one issue. The issue correctly bundles them, but the fix sequence matters: (1) scrub history, (2) remove the working-tree literal, (3) rotate the credential, (4) fix #83 so
admin123can never be a runtime default again. Doing them out of order is harmless but creates confusion about what is "done".Working-tree exposure is wider than the issue states.
admin:admin123appears in two lines of.claude/skills/transcribe/SKILL.md(lines 23 and 98). The issue mentions line 96 — that is slightly off, but both occurrences are in the same file and will be caught by the samefilter-reporun.README.md:60also shipsadmin123in plaintext (Password: admin123). This is in the working tree of the tracked fileREADME.md, reachable frommain. It is not mentioned in the issue but will be found bygitleaks detectand must be addressed in the same pass..claude/settings.local.jsoncontainsE2E_PASSWORD=admin123in 8allowListentries. Whether this file is tracked determines the history scope. It is present on disk but should be verified against.gitignorebefore the scrub — if tracked, it is another history hit.UserDataInitializer.java:37uses@Value("${app.admin.password:admin123}")— this is a Spring property default that will activate ifAPP_ADMIN_PASSWORDis not set. This is the root cause tracked as #83. It is explicitly out of scope for this issue but worth noting: until #83 is closed,admin123remains a valid runtime password even after the history scrub.docs/TODO-backend.mdcontainsadmin123in code snippets — another tracked file with the literal. Thefilter-reporeplace-text approach will catch it automatically.The
gitleaksscan identified commits3d3d4b86andfc27043d— both are the same "add personas/skills" commit appearing to exist on two branches (likely main and an old feature branch). Afterfilter-repo --replace-text, both will receive new SHAs. Any local clones and the remoteoriginatssh://git@heim-nas:222/must be force-pushed to.CWE-798 (Use of Hard-coded Credentials in Documentation) — low individual severity, high aggregate risk: if this repo is ever made public, or if a deploy script copies the value literally, the credential is permanently compromised.
Recommendations
Replace both working-tree occurrences first, commit, then scrub history — this keeps the working tree clean independently of the history rewrite, which can be done separately without time pressure.
Expand the
expressions.txtto cover all literal occurrences before runningfilter-repo:Be deliberate — a blanket replacement of
admin123everywhere may hit persona.mdfiles where the string appears as a counter-example (e.g.,security_expert.md:147). Scope the replacement to the specific files:.claude/skills/transcribe/SKILL.md,README.md,docs/TODO-backend.md.Verify
settings.local.jsonis in.gitignorebefore running the scrub.grep "settings.local" .gitignore— if it is not excluded, add it and include the file in thefilter-reporun.After force-push, rotate the credential immediately — treat
admin123as permanently public regardless of the scrub success. Generate a strong password (≥20 chars,openssl rand -base64 20), update.env, and redeploy.Add
gitleaks detectto the CI pipeline as a blocking step so no future commit can introduce credentials. The Gitea Actionsci.ymlis the right place — add it as the first step before the build.Add
APP_ADMIN_PASSWORDto.env.examplewith achange-meplaceholder so it is obvious to any new developer that this must be set. Currently it is absent from.env.example.Open Decisions
filter-reporeplacement: replacing the string everywhere in history (--replace-text) will rewrite every commit that containsadmin123— including persona files where it appears as a deliberate bad-example. Narrowing the scope with--pathto only the affected files is cleaner but requires a separate pass per file. Recommend the narrow approach to avoid rewriting commits that are intentionally instructional.admin123as the dev default. After #83 lands and the default is removed, the README must be updated to reflect the newAPP_ADMIN_PASSWORDpattern. This may belong in #83's scope rather than here.🏗️ Markus Keller — Application Architect
Observations
This is a documentation hygiene problem, not an architecture problem — the credential leaked through a
.claude/skill file that documents acurlexample, not through application configuration. The architectural problem (hardcoded Spring property default) is tracked separately as #83 and is correctly out of scope here.The two commits that introduced the leak (
3d3d4b86,fc27043d) exist on what appears to be main and at least one other branch.git filter-reporewrites the entire reachable history, so both are handled in one pass. The issue's approach is correct.filter-repois the right tool —bfgis easier for beginners but deprecated in practice;filter-repois the current Git project recommendation and handles branch/tag rewriting atomically.The
application.yamlSpring property default (${APP_ADMIN_PASSWORD:admin123}) is an architectural smell: the application can start in production with a known credential if the env var is absent. Spring's fail-fast alternative is${APP_ADMIN_PASSWORD:?APP_ADMIN_PASSWORD must be set}— this causes startup failure rather than silent insecure default. The audit doc already identifies this fix; it belongs in #83, not here.No ADR is needed for this issue — it is a remediation task, not an architectural decision. The process for scrubbing credentials from history is a one-time operational action.
Documentation impact: the issue does not mention that
README.mdalso containsadmin123as a publicly visible dev credential. That file is committed and on main. It should be covered in the same working-tree pass.Recommendations
Run the
filter-reposcrub as a single coordinated operation — do not split it across multiple PRs or force-pushes. One atomic rewrite, one coordinated force-push toorigin, one notification that all local clones (if any) must hard-reset. For a solo repo this is low coordination overhead.Scope the
expressions.txtnarrowly to the affected documentation files rather than a global string replace. A globaladmin123 ==> $ADMIN_PASSrewrite will touchsecurity_expert.mdanddevops.mdwhere the string appears as a deliberate negative example, changing the meaning of the documentation.Do not delete the skill file — the
curlexamples in.claude/skills/transcribe/SKILL.mdare functionally necessary. Replace the literal credential with$ADMIN_PASSand add a one-line comment pointing to.envorDEPLOYMENT.md. The skill stays useful.After the scrub, add
APP_ADMIN_PASSWORDto.env.examplewith achange-mevalue and a comment. Currently that file documents PostgreSQL, MinIO, and OCR tokens but omits the admin password entirely — which is why it gets forgotten.Treat the history rewrite as a one-way door — once
filter-reporewrites and the force-push lands, all old SHAs are gone. Verify thegitleaks detectclean output before the push. Roll back is not possible after the remote is updated without a backup of the original reflog (whichfilter-repoprovides inrefs/filter-repo/).Tag the post-scrub HEAD as a known-clean baseline — e.g.,
git tag security/history-scrub-460after the force-push. This gives future forensic investigation a named point that is known to be clean.⚙️ Tobias Wendt — DevOps & Platform Engineer
Observations
The credential is in the git history on the
originremote (ssh://git@heim-nas:222/marcel/familienarchiv.git). The working-tree fix alone does nothing for the history — the remote must receive a force-push afterfilter-reporewrites local history.settings.local.jsonhasE2E_PASSWORD=admin123in 8 BashallowListentries. If this file is tracked (it is present on disk;.gitignorehas.envexcluded but not necessarilysettings.local.jsonexplicitly), it is another history hit. Rungit ls-files .claude/settings.local.jsonto confirm tracking status. If tracked, include it in the scrub.The CI workflow
.gitea/workflows/ci.ymldoes not hardcode the credential — looking at the infrastructure docs (docs/infrastructure/ci-gitea.md), the CI correctly passes${{ secrets.E2E_ADMIN_PASSWORD }}via Gitea secrets. This is the right pattern and is not affected by this issue.APP_ADMIN_PASSWORDis absent from.env.example— the example env file documents PostgreSQL, MinIO, Mailpit, and OCR tokens, but not the admin password. This is a gap: anyone standing up the app from scratch will getadmin123by default without realizing it. AddingAPP_ADMIN_PASSWORD=change-meto.env.examplecloses this gap.Docker Compose does not pass
APP_ADMIN_PASSWORDto the backend container in the currentdocker-compose.yml(nothing visible in the grep). If the env var is not wired through Compose, the Spring fallbackadmin123will always be used in local dev — exactly the wrong default.No self-hosted Renovate or secret-scanning CI step is currently visible. After the scrub, a
gitleaks detectCI step should be the first gate inci.ymlto prevent future leaks.Recommendations
Before the
filter-reporun, confirm tracking status of all candidate files:Only rewrite what is actually tracked — untracked files in working tree do not propagate.
Add
APP_ADMIN_PASSWORD=change-meto.env.exampleimmediately (can be done as a normal commit before the history rewrite — it does not add a secret, it adds a placeholder). Also wire it throughdocker-compose.ymlunder the backend service'senvironment:block usingAPP_ADMIN_PASSWORD: ${APP_ADMIN_PASSWORD}.Back up the remote's reflog before force-pushing:
filter-repoautomatically saves the original refs torefs/filter-repo/locally, but having a named branch on the remote as a safety net costs nothing and enables recovery if the push goes wrong.Add
gitleaks detect --source . --exit-code 1as the first step in.gitea/workflows/ci.ymlusing thegitleaks/gitleaksDocker image. This turns the scan into a mandatory gate on every PR and push, preventing the same class of problem from recurring.After the scrub and force-push, verify the remote is clean:
Cloning fresh from the remote is the definitive test — it proves the rewritten history propagated correctly.
Update the worktrees under
.worktrees/— the issue notes these contain working-tree occurrences. Either delete and recreate them from the cleaned remote, or run the replace in them manually. They do not affect the remote history but will confuse localgitleaksscans.🧪 Felix Brandt — Senior Fullstack Developer
Observations
The literal replacement in
SKILL.mdis a documentation change, not a code change — no tests required for that part. The credential inSKILL.mdlines 23 and 98 is in acurlexample that Claude reads at runtime; replacing it with$ADMIN_PASS+ a comment is safe and does not change any application behavior.UserDataInitializer.java:37(@Value("${app.admin.password:admin123}")) is the application-code side of this problem and belongs to #83. Worth noting that this class also hasreader123hardcoded at line 99 for the E2E reader user — a similar smell that should be addressed when #83 is tackled. The E2E user passwords should come from an env var or a test-specific profile override, not from literals in the initializer.The
README.mdhasPassword: admin123at line 60 — this is a tracked, publicly visible file. The fix here is to replace it with something likePassword: [set via APP_ADMIN_PASSWORD]with a pointer toDEPLOYMENT.md. This is a one-line edit and should be in the same working-tree cleanup commit.docs/TODO-backend.mdcontains code snippets withadmin123. These look like historical notes, not active code. They are tracked and will appear ingitleaksoutput.No E2E tests reference
admin123directly in the test files I can check — the CI workflow docs show${{ secrets.E2E_ADMIN_PASSWORD }}is used. Thesettings.local.jsonallowlist entries withE2E_PASSWORD=admin123are for local dev convenience; if the file is untracked these have no git history impact.Recommendations
Make the working-tree fix a clean, atomic commit before the history rewrite:
.claude/skills/transcribe/SKILL.mdlines 23 and 98: replaceadmin:admin123withadmin:$ADMIN_PASS+ add comment on line above:README.md: replacePassword: admin123withPassword: [see APP_ADMIN_PASSWORD in .env]docs/TODO-backend.md: replaceadmin123occurrences with$ADMIN_PASSsecurity(docs): replace admin:admin123 literals with $ADMIN_PASS placeholderAfter the history rewrite, the working-tree commit will be rewritten too — this is fine. The important thing is that the commit exists on the clean HEAD before the force-push so the current state of files is correct.
reader123inUserDataInitializer.java:99should be added to #83's scope — it is the same class of problem (hardcoded credential in tracked source) and should be fixed at the same time as the admin password default removal.Verify no E2E test file hardcodes the password — run
grep -rn "admin123" frontend/e2e/before closing this issue. From what I can see the tests useprocess.env.E2E_PASSWORD, which is correct.docs/TODO-backend.mdappears to be a development scratchpad — check whether it should remain tracked at all, or whether it belongs in.gitignore. If it is kept tracked, clean it up.📋 Elicit — Requirements Engineer
Observations
The issue is well-specified for a remediation task. It has a clear context, a two-step approach, verification steps, and acceptance criteria. The effort estimate (S, 1 hour for the scrub) is realistic for the
filter-repostep alone. The rotation step depends on #83, which is correctly flagged as a dependency.Scope gap: additional tracked files. The issue specifies
.claude/skills/transcribe/SKILL.mdandexpressions.txt(one-shot) as the only affected files. In fact,README.mdanddocs/TODO-backend.mdalso containadmin123as tracked files. The acceptance criteria — "No occurrence ofadmin:admin123in any tracked file" — will fail unless these are included in the working-tree fix. The issue should explicitly list all files to be modified.Acceptance criterion ambiguity: "any commit reachable from main or any branch." The repo has 30+ branches (visible from
git branch -a). Thefilter-reporun rewrites all branches by default, so this criterion is met automatically — but it should be confirmed explicitly. The verification step should be:git log --all -p | grep -c "admin:admin123"→ 0, not just checkingmain.Missing acceptance criterion: the remote. The working tree and local git history can be clean while the remote (
origin) still has the old commits. The acceptance criteria should include: "Remoteoriginhas been force-pushed and a fresh clone produces 0gitleaksfindings."Dependency on #83 is correctly modeled — the rotation step is explicitly blocked by #83. However,
APP_ADMIN_PASSWORDbeing absent from.env.exampleis a separate gap that does not depend on #83 and could be closed immediately. This should be an acceptance criterion here, or a sub-task.The effort estimate excludes credential rotation. If #83 is worked in parallel, the total effort is more than 1 hour. This is acknowledged in the issue ("The rotation depends on #83"), but the estimate should note that the full remediation (scrub + rotation) is an M not an S.
Recommendations
Add a pre-work checklist to the issue (or this comment serves as a supplement):
admin123:SKILL.md,README.md,docs/TODO-backend.md,docs/DEPLOYMENT.md(informational references, not literal credentials — verify each)git ls-files <file>for eachgrep -rn "admin123" frontend/e2e/Add "remote origin updated" as an explicit acceptance criterion — the scrub is not done until the remote is clean and a fresh clone confirms it.
Add "APP_ADMIN_PASSWORD added to
.env.example" as an acceptance criterion — this is independent of #83 and closes a real gap in the onboarding documentation.Clarify the scope of
expressions.txtin the issue — document exactly which string patterns will be replaced to avoid accidentally rewriting persona.mdfiles whereadmin123appears as a counter-example.The issue title says "scrub from git history" — this is correct, but the body conflates the history scrub with the working-tree literal replacement. Separating these into two numbered items (as the issue does) is the right structure; just ensure the acceptance criteria reflect both tracks independently.
🧪 Sara Holt — QA Engineer & Test Strategist
Observations
No test coverage is needed for the
filter-repohistory rewrite itself — it is an operational action, not a code change. But the verification of the scrub is a testable activity, and the issue's verification steps are a good informal test plan.The issue's verification step 3 (
git log -p .claude/skills/transcribe/SKILL.md | grep -c admin:admin123 → 0) checks only one file. A more thorough check covers all tracked files:git log --all -p | grep "admin:admin123" | wc -l. This should be ≥0 before the scrub and exactly 0 after.gitleaks detect --source .vsgitleaks detect --no-git— the issue usesgitleaks detect --source .(history scan) andgitleaks dir .(working tree). The correct working-tree command isgitleaks detect --no-git(orgitleaks detect --source . --no-git).gitleaks dirwas the old CLI; currentgitleaksv8+ usesdetect --no-git. Verify the installed version to use the correct flags.The worktrees under
.worktrees/and.claude/worktrees/contain working-tree occurrences. These are not in git history (as the issue correctly notes), but if a worktree-awaregitleaksscan runs, it may flag them. The acceptance criterion "Worktrees either deleted or refreshed" is correct — deletion is easier and safer.Regression prevention: the issue does not specify a permanent gate to prevent recurrence. Once the scrub is done, nothing stops the next
curlexample from hardcodingadmin123again. Agitleakspre-commit hook or CI gate closes this.The E2E suite currently passes
E2E_PASSWORDvia environment variable — this is the correct pattern. There is no test regression risk from this issue.Recommendations
Use a comprehensive post-scrub verification script rather than individual commands:
Add a
gitleakspre-commit hook post-scrub usingpre-commitframework or a simple.git/hooks/pre-commitscript. This is the cheapest permanent gate.Add
gitleaks detect --exit-code 1as step 1 of.gitea/workflows/ci.yml— before any build steps. A credential in a PR should fail CI immediately, not at the end of a 10-minute pipeline.For the
.env.exampleaddition: verify thatAPP_ADMIN_PASSWORD=change-meis added and that a CI test step (or linting rule) catches when.env.exampleis missing a key thatapplication.yamlexpects. This is lightweight "documentation test" but prevents the same gap from recurring.Delete the worktrees rather than refreshing them — refreshing a worktree after a history rewrite requires
git worktree remove && git worktree add, which is equivalent to deletion and recreation. Deletion is simpler and leaves no stale refs.🎨 Leonie Voss — UI/UX Design Lead
Observations
This issue is entirely a git history and configuration hygiene task — no UI/UX surface is affected.
SKILL.mdis an internal Claude skill document,UserDataInitializer.javacontrols seeded data, andapplication.yamlcontrols startup configuration. None of these produce user-visible output.I have no design findings for this issue. The credential exposure does not create any user-facing UX failure — the risk is entirely operational (unauthorized admin access if the default password reaches a live deployment).
One tangential note from a user experience of the system perspective: If a developer accidentally deploys with
admin123as the password and an attacker gains admin access, they could modify documents, delete family history, or change user permissions. The UX consequence of that attack is severe — irreversible data loss for the family. This is a reason to prioritize the companion issue #83 (fail-fast startup whenAPP_ADMIN_PASSWORDis unset) alongside this scrub, not to delay it.Recommendations
docs/DEPLOYMENT.mdto remove theadmin123default reference from the onboarding checklist and replace it with clear instructions for generating a strong password — this is a UX concern for the developer experience (onboarding clarity), not the end-user experience.🗳️ Decision Queue — Consolidated Open Decisions
Two genuine tradeoffs raised across the review require a human call.
D1 — Scope of
expressions.txt: narrow (per-file) vs. broad (global string replace)Raised by: Nora (Security), Markus (Architect)
The tradeoff:
Broad replace (
admin123 ==> $ADMIN_PASSglobally across all history): one-shot, catches everything including files not yet identified. Side effect: rewrites commits insecurity_expert.mdanddevops.mdwhereadmin123appears as a deliberate bad-example in the persona documentation, changing the meaning of those documents.Narrow replace (scoped to
--path .claude/skills/transcribe/SKILL.md --path README.md --path docs/TODO-backend.md): requires onefilter-repoinvocation per file path (or a combined--pathlist), but leaves intentional counter-examples in persona files untouched.Recommendation: Use the narrow approach. Add
--pathflags for exactly the files that contain credentials-as-credentials (not credentials-as-examples). The persona files are instructional; rewriting them creates documentation confusion and adds unnecessary rewrites to commits that were correct.D2 — README.md treatment: fix here or in #83?
Raised by: Nora (Security), Markus (Architect)
The tradeoff:
README.md:60currently readsPassword: admin123as the documented dev default. This line should change, but when:Password: [set via APP_ADMIN_PASSWORD in .env]. Closes the tracked-file exposure now. Leaves a slightly misleading README until #83 removes the Spring fallback.APP_ADMIN_PASSWORDas the only source of truth, which is only true after #83 lands.Recommendation: Fix the literal in README.md here (it is a tracked file with the credential in history that
gitleakswill flag), but scope the messaging update — replaceadmin123with a placeholder now, and update the surrounding onboarding text (what to set, where to set it) in #83 when the default is removed. The two-pass approach is slightly more work but keeps each issue self-contained.