Skip to content

Commit af20d74

Browse files
dancinlifedancinlabclaude
authored
fix(get): append trailing newline only on interactive TTY (#9)
`secret get` omits the trailing newline for pipe-friendliness, but in an interactive zsh that shows a reverse-video `%` missing-newline marker after the value. Add `[ -t 1 ] && printf '\n'` so a newline is emitted ONLY when stdout is a terminal; piped / command-substitution output stays byte-exact. Co-authored-by: dancinlab <noreply@dancinlab> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a791099 commit af20d74

3 files changed

Lines changed: 7 additions & 1 deletion

File tree

‎ARCHITECTURE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ secret set <key> <value> secret get <key>
5151

5252
- **Input** — value via argv (low-risk), stdin (whole stream, multiline/ssh-key safe), or hidden tty prompt. argv refused for the master password and for high-value secrets.
5353
- **Processing** — store.enc is decrypted to memory only; plaintext is never written to disk. Entries are `<key><TAB>base64(value)`; base64 keeps newlines / tabs / text-binary line-safe.
54-
- **Output** — `get` prints exactly one value to stdout. `rotate` prints only a sentinel. `list` prints keys. Every mutating op optionally commits + pushes the ciphertext.
54+
- **Output** — `get` prints exactly one value to stdout (byte-exact, no trailing newline when piped so it stays pipe-friendly; a trailing newline is added only on an interactive TTY, `[ -t 1 ]`, to suppress zsh's `%` missing-newline marker). `rotate` prints only a sentinel. `list` prints keys. Every mutating op optionally commits + pushes the ciphertext.
5555

5656
## Governance
5757

‎CHANGELOG.jsonl‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{"ts":"2026-07-11","title":"get: TTY-only trailing newline (kill zsh % marker)","body":"get: append a trailing newline only on an interactive TTY (\\`[ -t 1 ]\\`) so the value no longer shows zsh’s \\`%\\` missing-newline marker; piped / command-substitution stdout stays byte-exact (no newline) — pipe-friendliness preserved."}
12
{"ts":"2026-06-27","title":"chore(changelog): CHANGELOG.md -> CHANGELOG.jsonl (append-only JSONL)","body":"History SSOT moved from freeform CHANGELOG.md to CHANGELOG.jsonl (newest-first JSONL · {ts,title,body}) via sidecar changelog migrate (secret); gate rewired + doc pointers updated."}
23
{"ts":null,"title":"2026-06-15","body":"- **chore(harness): perfect harness setup** — Brought the repo to full `dancinlab/harness` (harness-hardcore) conformance.\n - **ARCHITECTURE.md** rewritten as the real update-in-place SSOT (overview · component map of the single `bin/secret` bash script + subcommands + encrypted-file backend · set/get → openssl → store.enc data flow · governance). Replaces the stub.\n - **CLAUDE.md** converted from a symlink (pointed outside the repo) into a harness-standard markdown guide — H1 + project blurb + `## Structure` tree + governance summary + `## Harness` + Quick reference. The machine-readable directives are preserved in `project.tape`.\n - **harness.config.json** — `stack` → generic (single bash script), `verify` → `bash -n bin/secret` syntax check, `lockdown.files` → `bin/secret` (credential core), changelog trigger scoped to the core script, `protectedBranches` [main, master], added the `docs` block (architecture/log/scratchDir/scopeDirs/allow).\n - **`.harness-engine`** submodule bumped to the latest `harness-hardcore` engine.\n - `harness docs check` → `docs: ok`; `harness verify` → 1/1 passed; CLAUDE-MD violations 0."}
34
{"ts":null,"title":"2026-05-31","body":"- **0.6.1 — auto-backup 실패 묵살 버그 수정 (commit 실패 → 가시 경고 + git 신원 힌트)** — `_maybe_auto_backup`이 `set`/`rotate`/`delete` 후 store.enc를 commit·push하는데, 실패 경로(cp·cd·`git add`·`git commit`)가 전부 `|| exit 1`로 **조용히 subshell만 탈출**(`) || true`가 삼킴)해 경고가 안 떴다. git push 실패만 경고하고 정작 commit 실패는 묵살 → store.enc가 로컬만 바뀌고 vault 미반영인데 사용자는 모름. 특히 git 신원(`user.email`/`user.name`) 미설정 호스트(mini 등)에서 매 `secret set`이 조용히 vault 동기 실패. 이제 모든 실패 경로가 stderr에 명시 경고를 찍고, commit 실패 시 git 신원 설정 + `secret sync` 후속 명령을 안내한다. 동작 변화 없음(여전히 non-fatal로 로컬 write는 보존) — 가시성만 추가. 백엔드를 macOS Keychain(`security` CLI)에서 단일 openssl 암호화 파일 `store.enc`로 교체. Keychain은 GUI(Aqua) 보안세션에 바인딩돼 ssh·헤드리스에서 `secret get`이 빈값을 반환했는데(`User interaction is not allowed`), 새 백엔드는 세션 무관 파일이라 Mac·mini(ssh)·linux 어디서나 동작.\n - **암호화**: `openssl enc -aes-256-cbc -pbkdf2 -salt -iter 200000`. 마스터 비밀번호는 fd(process substitution)로 전달 — argv 노출 없음.\n - **store 포맷**: 평문은 라인당 `key<TAB>base64(value)`. base64 인코딩으로 멀티라인·ssh키·텍스트 바이너리(PEM/cert) 안전.\n - **마스터 비밀번호 소스**: `$SECRET_MASTER`(env) → `~/.config/secret/master`(0600 파일) → tty 프롬프트. argv 절대 미허용. 헤드리스: `SECRET_MASTER=… secret get <key>`.\n - **평문 디스크 기록 0**: 복호화는 변수/stdout(in-memory)만. `set` = 복호화→수정→재암호화→원자적 write(ciphertext `mktemp .store.enc.* → mv`). 평문 임시파일 없음.\n - **keychain 제거**: get·set·rotate·check·list·delete hot-path의 `security find-generic-password`/`add-generic-password` 전부 제거. `security` 잔존은 `migrate --from-keychain` 1회용 reader에만.\n - **verb surface 유지**: get·set·rotate·check·delete·list·backup·sync·init·migrate (CLI 호환). high-value 보호(BIP39·xprv·WIF refuse), rotate(`openssl rand`), `SECRET_BACKUP_AUTO` 자동 백업, git backup/sync 로직 보존.\n - **migrate 재정의**: `secret migrate --from-keychain [--purge-source]` — 구 keychain 전 키를 store.enc로 1회 import + 라운드트립 검증. 키체인 read는 GUI 세션 필요 → Mac Terminal.app에서 실행. 기존 `dancinlab.keychain-db`는 롤백 안전망으로 보존.\n\n---"}

‎bin/secret‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ _store_get() {
142142
line=$(_store_decrypt | awk -F'\t' -v k="$key" '$1==k{print $2; found=1} END{exit !found}') || return 1
143143
[ -n "$line" ] || return 1
144144
printf '%s' "$line" | base64 -d
145+
# Append a trailing newline ONLY on an interactive terminal, so the value sits
146+
# on its own line (no zsh `%` missing-newline marker). Piped / command-subst
147+
# stdout stays byte-exact — no newline — keeping `get` pipe-friendly (@get).
148+
[ -t 1 ] && printf '\n'
149+
return 0
145150
}
146151

147152
# _store_has <key> → exit 0 if present, 1 otherwise. No value output.

0 commit comments

Comments
 (0)