Posts in category “Tips”

重启后纯 SSH 登录,gh 突然 401?token 可能锁在 keyring 里

机器重启后,gh 命令全线 HTTP 401: Requires authentication,可你明明早就 gh auth login 过。奇怪的是,好像"重启一下授权就没了"。

真凶多半是:token 存进了 系统 keyring(libsecret/gnome-keyring),而你走的是纯 SSH(publickey)登录。SSH 登录时 PAM 拿不到你的登录密码,pam_gnome_keyring 不会解锁 keyring——只有桌面/GUI 登录才会顺带解锁。于是重启后 keyring 一直锁着,gh 读不出 token,直接 401。这跟"重启"本身无关,是"重启后再没有任何桌面登录去解锁 keyring"。

无头/纯 SSH 机器上,别把凭据托付给 keyring。最省心的是用环境变量,gh 优先读它,完全不碰 keyring、也不落 hosts.yml:

# ~/.bashrc.secret (被登录 shell source)
export GH_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx

验证时别把 token 打出来,看它解析到哪个账号就行:

gh api user -q .login      # 打印用户名即成功

GH_TOKEN 随登录 shell 加载,重启、纯 SSH 都稳。清掉 keyring/hosts.yml 里那份冗余凭据(gh auth logout)可保持单一来源。

更广的教训:无头机器上,任何依赖"交互式桌面会话"的机制——keyring 解锁、user 级 systemd 服务常驻——都会踩坑,优先选不依赖会话的方案(环境变量、loginctl enable-linger)。

Pinning a gh account with `GH_TOKEN="$(gh auth token --user X)"`? Check it's non-empty

A common pattern for scripts that must always hit GitHub as a specific account, regardless of whatever account is currently active, is:

GH_TOKEN="$(gh auth token --user work-account)" gh workflow run ...

This works — until gh auth token --user work-account fails and returns an empty string. GH_TOKEN="" is not treated as "unset" by your shell, but it is treated as unset by gh itself, which then silently falls back to whatever account is currently active in the keyring. If that's your personal account and the target repo belongs to an org it can't see, you get a confusing HTTP 404: Not Found — which reads like a missing-repo problem, not an auth problem.

Guard the lookup instead of trusting it inline:

TOKEN="$(gh auth token --user work-account)"
if [ -z "$TOKEN" ]; then
    echo "Error: could not obtain gh token for work-account" >&2
    exit 1
fi
GH_TOKEN="$TOKEN" gh workflow run ...

Same fix applies to any wrapper that routes gh calls by repo owner: resolve the token first, fail loudly if it's empty, and only then invoke the real gh binary. Silent fallback to the active account is the failure mode to design out.

Excalidraw's hand-drawn look is free — Excalidraw+ only buys you cloud collab

I almost passed on the cute hand-drawn flowchart style, assuming it sat behind a subscription. It doesn't. The hand-drawn aesthetic is Excalidraw's default and only style, and it's completely free. The paid tier, Excalidraw+, is team cloud collaboration and version history — nothing to do with how the diagrams look.

mermaid.live is the same story: the official open-source editor for Mermaid.js, free, no credits, no pay-per-render. The "credit-based" impression usually comes from third-party SaaS tools that generate Mermaid from a prompt.

So going from a flowchart to a hand-drawn version costs nothing. Get your flowchart as Mermaid (write it, or hand a vision-capable AI a photo of your sketch), then paste it straight onto the Excalidraw canvas — it detects the syntax and pops a "Parse as Mermaid" import dialog. There is no Insert → Mermaid item in the hamburger menu or toolbar (those only have Open / Save / Export / Live Collaboration); the other entry point is the command palette (Ctrl/Cmd + /) → search Mermaid.

ECS force-new-deployment vs scaling desiredCount to 0 then 1

Need to restart an ECS service to pick up a changed SSM Parameter Store value (env vars/secrets are resolved when a task starts, so any fresh task picks up the new value). Two ways to force a restart look equivalent but aren't.

aws ecs update-service --force-new-deployment runs a normal rolling deployment, governed by the service's deploymentConfiguration:

aws ecs update-service --cluster my-cluster --service my-service --force-new-deployment

desiredCount never changes. If maximumPercent is above 100 (e.g. 200%), ECS starts the new task first, waits for it to pass the ALB health check, then drains and stops the old one — new and old run side by side for a moment, so there's effectively zero downtime.

Scaling desiredCount to 0 and back to 1 is a hard stop-then-start: every running task is killed first, the target group is empty until the new task comes up and passes health checks, and anything hitting the service in that window fails. It also completely bypasses the rolling-deployment logic — there's no overlap to make it graceful.

Same end state (new task, new config), different path to get there. Two things to check before relying on force-new-deployment for a "safe" restart: maximumPercent needs to allow >100%, or you get the same stop-then-start behavior with the failure mode you were trying to avoid; and deploymentCircuitBreaker — if it's disabled, a broken new task version just cycles and retries forever without rolling back, while the old task quietly keeps serving traffic, so the deployment looks non-disruptive but never actually finishes.

Three-agent caucus before opening a design ticket

A solo design pick ships a brittle ticket. A small jury forces you to surface the hard rule, spot when "labeling problem" is really a data gap, and converge to ready instead of refining.

When to convene

Caucus is for design questions with real divergence, where the goal is a ready ticket. Skip it for one-line facts (one agent), pure execution (just do it), or where there's only one sane option.

Composition: two advocates + one skeptic

Two advocates pick majority-wins and call it consensus. Add an impartial chair/skeptic whose job is failure modes and a decision criterion. Three is convention, not doctrine — two advocates + one chair works; one advocate + one skeptic works; three advocates doesn't.

The three prompts share one SCENARIO

Each agent gets the same verified facts (IDs, code-line refs, observed outcomes). Letting each agent self-research drifts the facts and you can't synthesize. Then each gets a distinct lens and a boundary:

SCENARIO (verified): ...
CONTEXT (read these files, these are the related issues, decision-maker's steer): ...
YOUR LENS: [extend-X | separate-track | skeptic/chair]
BOUNDARY: read-only, no code, ≤500 words, return VERDICT line.

Use VERDICT as the last line so synthesis can grep it. Pick lens names that frame the choice (extend-#122 advocate, separate-track advocate, skeptic/chair) — the framing shapes the output.

Run the three concurrently in background; wait for all three before synthesizing. Sequential loses the wall-clock and the parallel disagreement.

Synthesize

  1. Consensus → hard rule. Whatever all three agree on goes in as a non-negotiable constraint, not a soft preference.
  2. Divergence → chair's criterion + compatibility points. The skeptic's job is to name when each advocate breaks. Often the advocates are compatible once you apply the criterion (e.g. fallback rule + quarantine path, gated by data-first verification).
  3. Lock → ready. The whole point of the caucus is to converge. If you converged, open ready and release the lock. Don't re-refine.

Traps

  • Treating caucus as solution. Three agents return positions; you synthesize. Letting them agree with each other is a fragile consensus.
  • No skeptic. Two advocates ship majority-wins.
  • No shared SCENARIO. Facts diverge, synthesis collapses.
  • No VERDICT line. Agent writes an essay, you can't grep.
  • Caucus for execution. Locating a bug, writing a doc, picking a flag value — none of these need a jury.
  • "Three" as dogma. Two advocates + one chair is the minimum useful shape. Three advocates is the maximum useless shape.

The synthesis rule that matters most: when the chair says "this is a data problem disguised as a labeling problem," that's the reframe — verify the data first, then design the fallback. Without the skeptic, you'd have built a clever rule on top of an incomplete export and shipped proxy labels.