Skip to content

Add HackMD API token secret - #59

Open
alycda wants to merge 10 commits into
mainfrom
claude/hackmd-api-token-secret-njlvig
Open

Add HackMD API token secret#59
alycda wants to merge 10 commits into
mainfrom
claude/hackmd-api-token-secret-njlvig

Conversation

@alycda

@alycda alycda commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Scaffolds a new agenix secret for the HackMD API token, following the existing personal/ secret pattern (git-config, agent-instructions).

Changes

  • secrets/secrets.nix — declare personal/hackmd-api-token.age with the personal age recipient key
  • secrets/personal/hackmd-api-token.age⚠️ PLACEHOLDER, not real ciphertext. Replace with the armored age encryption of the token before switching:
    rage -e -a -r age1mxz3lqtpxg35s2cct2gex76l66wrw9xpv5v8tk340gqxsdzxh5msq8vp09 \
      -o secrets/personal/hackmd-api-token.age <plaintext-token-file>
    
    (or interactively: agenix -e secrets/personal/hackmd-api-token.age)
  • home-manager/modules/tools/hackmd.nix — new tool module wiring the secret; decrypts to ~/.local/share/agenix/hackmd-api-token via the secretsDir/identityPaths already configured in modules/git.nix
  • home-manager/profiles/home.nix — import the module in the personal profile only. Other profiles can opt in with the same one-line import if the token is wanted there.

Notes

  • home-manager switch on a profile importing this module will fail at decryption until the placeholder is replaced with real ciphertext.
  • Consumers should read config.age.secrets.hackmd-api-token.path at use time (e.g. export HMD_API_ACCESS_TOKEN="$(cat ~/.local/share/agenix/hackmd-api-token)") rather than baking the token into the environment.

🤖 Generated with Claude Code

https://claude.ai/code/session_01G4o4LduK88LUqj8kb2Z4LU


Generated by Claude Code

Declare a new agenix secret for the HackMD API token, following the
existing personal/ secret pattern (git-config, agent-instructions):

- secrets/secrets.nix: declare personal/hackmd-api-token.age with the
  personal age recipient key
- secrets/personal/hackmd-api-token.age: PLACEHOLDER file, not real
  ciphertext - to be replaced with the armored age encryption of the
  token before switching (rage -e -a -r <key>, or agenix -e)
- home-manager/modules/tools/hackmd.nix: new tool module that wires the
  secret; decrypts to ~/.local/share/agenix/hackmd-api-token via the
  secretsDir/identityPaths already configured in modules/git.nix
- home-manager/profiles/home.nix: import the module in the personal
  profile (the token is personal-keyed; other profiles can opt in later)

Consumers should read config.age.secrets.hackmd-api-token.path at use
time rather than baking the token into the environment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4o4LduK88LUqj8kb2Z4LU
Comment thread home-manager/modules/tools/hackmd.nix Outdated
Comment thread secrets/personal/hackmd-api-token.age Outdated
alycda and others added 9 commits July 30, 2026 00:52
- hackmd.nix: `_:` -> `{ config, ... }:` (was referencing undefined `config`)
  and add the missing closing brace; the module didn't parse.
- Point `file` at secrets/personal/ and `git mv` the .age there, matching
  secrets.nix (which declares personal/) and the existing personal/ pattern.
  Previously secrets.nix declared personal/ while the file + module used work/,
  so agenix could never produce ciphertext at the path the module reads.
- Document that the decrypted content must be env-file format
  (`HACKMD_API_TOKEN=<token>`) at ~/.config/hackmd/env, so the Claude Code
  hackmd MCP launcher's `. ~/.config/hackmd/env` sources it correctly.
- Fix stale comments claiming ~/.local/share/agenix/hackmd-api-token and the
  wrong HMD_API_ACCESS_TOKEN var name.

Placeholder ciphertext still needs replacing with real ciphertext before
`home-manager switch`:
  agenix -e secrets/personal/hackmd-api-token.age   # type: HACKMD_API_TOKEN=<token>

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The token is a work account key, and this repo separates secrets by profile
(single age recipient; personal/ vs work/ is organizational). The work Mac
(darwinConfigurations."ditto", host DTO-A311-evans) and work devcontainer
(alyssa@work-dev) both run the work home profile; the personal laptop
(alyssa@home) runs home.nix.

- Move the secret to secrets/work/hackmd-api-token.age and declare it there in
  secrets.nix (was personal/).
- Import the module from work.nix (this machine) instead of home.nix, so the
  work token deploys where the hackmd MCP actually runs. Net vs main: hackmd is
  added only to the work profile.

Real ciphertext still required before switch:
  agenix -e secrets/work/hackmd-api-token.age   # type: HACKMD_API_TOKEN=<token>

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Some machines use a personal HackMD account, others the work account. Wire a
symmetric parallel so each profile mounts its own account's key to the same
~/.config/hackmd/env path the MCP launcher sources.

- Rename hackmd.nix -> hackmd-work.nix (work account, imported by work.nix).
- Add hackmd-personal.nix (personal account, imported by home.nix).
- Declare secrets/personal/hackmd-api-token.age alongside work/ in secrets.nix
  (placeholder ciphertext, replace via agenix -e before switch).

Both modules define age.secrets.hackmd-api-token at the same decrypt path, so
they are mutually exclusive — exactly one per profile (work.nix XOR home.nix).

Fill in the personal token with:
  agenix -e secrets/personal/hackmd-api-token.age   # type: HACKMD_API_TOKEN=<token>

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The personal HackMD key is now the actual credential rather than the
shared placeholder ciphertext both accounts started from. Verified it
decrypts under the personal identity and is in the env-file format the
launcher sources (HACKMD_API_TOKEN=...).

The work blob is deliberately untouched — it still carries the original
placeholder and needs the same treatment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds yuna0x0/hackmd-mcp as a module imported by the two account modules,
so the server and the token it needs always arrive together and pick up
whichever account the profile decrypted.

The token never reaches the repo or ~/.claude.json: the launcher sources
~/.config/hackmd/env at spawn time. An `env` block in the MCP entry would
put the credential in world-readable plaintext, which is the thing agenix
exists to prevent.

Why an activation script rather than home.file: Claude Code has no
declarative user-scope MCP config. settings.json has no mcpServers key and
.mcp.json is project-scoped, so a user-scope server must live in
~/.claude.json — a file Claude Code rewrites constantly. Symlinking a
read-only store path over it would break the app, so we converge the one
key we own with jq, the same idempotent approach claude-code.nix already
uses for the CLAUDE.md import line.

Version is pinned: bare `npx -y hackmd-mcp` resolves to whatever is newest
at spawn time, so the tool surface could change between two sessions on an
unchanged flake — the opposite of what pinning nixpkgs buys.

Verified end-to-end: the launcher answers an MCP initialize handshake as
hackmd-mcp 1.5.7 with the token sourced from the agenix env file, and
fails with a clear message when the secret is absent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`agenix -e` failed on every secret with "Tried to reset after the
underlying buffer was exceeded", pointing at src/age.rs — so the obvious
read was a corrupt .age file. It wasn't. The error string lives in the age
crate's identity-file parser, and the real cause was one missing byte at
the end of ~/.age/personal-key.txt.

Two dead ends are recorded because both are reasonable and both are wrong:
the corrupt-ciphertext theory (killed by rage decrypting the same file
fine), and pinning ragenix to a known-good version — there isn't one.
ragenix vendors age 0.10.1 at HEAD and the fix landed in age 0.11.0, so it
needs a dependency bump, not an input bump.

Session history supplied the provenance the current session couldn't: the
key was pasted from a password manager rather than generated by
age-keygen, which is exactly the step that omits the trailing newline, and
the container injection path is byte-exact — so the durable fix is the
host copy, not this container.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An agent reading CLAUDE.md had no way to learn the knowledge store exists;
`rg docs/solutions` over the repo returned nothing. The store only
compounds value if it gets found, and fresh sessions and collaborators
without the plugin are exactly who need the pointer.

One line in the existing structure tree rather than a new section — the
smallest change that names the folder, its organisation, and the
frontmatter fields worth searching on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four conflicts, all from the branch being 20 commits behind:

- secrets/secrets.nix — purely additive on both sides. Kept both blocks
  and reworded the HackMD comment to point at the Linear convention above
  it, since both now use directory-carries-the-identity.

- home.nix / work.nix — main moved agent-skills.nix into common.nix, so
  its import was dropped from both profiles and only the hackmd-* import
  was kept. Keeping it would have duplicated what common.nix already does.

- CLAUDE.md — took main's side wholesale. It already documents
  docs/solutions/, and more thoroughly than the line added on this branch
  (it also covers CONCEPTS.md and describes when the store is relevant),
  so that commit is now a no-op. Its structure block also corrects the
  Dockerfile/docker entries for multi-arch.

Verified after resolving: statix, deadnix, and `nix flake check
--all-systems` all pass, and both the work and home profiles evaluate with
the hackmd modules imported.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

⊕ Entity-level changes

docs/solutions/runtime-errors/ragenix-edit-fails-on-identity-without-trailing-newline.md

Status Type Name
+ preamble (preamble)
+ heading ragenix fails on every secret when the age identity file has no trailing newline
+ heading Problem
+ heading Symptoms
+ heading What Didn't Work
+ heading Solution
+ heading Why This Works
+ heading Prevention
+ heading non-empty output means the last byte is NOT a newline
+ heading Related Issues

home-manager/modules/tools/hackmd-mcp.nix

Status Type Name
+ orphan module-level
+ binding version
+ binding launcher
+ orphan module-level
+ binding register
+ orphan module-level
+ binding home.packages
+ orphan module-level
+ binding home.activation.hackmdMcpServer
+ orphan module-level

home-manager/modules/tools/hackmd-personal.nix

Status Type Name
+ orphan module-level
+ binding imports
+ binding age.secrets.hackmd-api-token
+ orphan module-level

home-manager/modules/tools/hackmd-work.nix

Status Type Name
+ orphan module-level
+ binding imports
+ binding age.secrets.hackmd-api-token
+ orphan module-level

home-manager/profiles/home.nix

Status Type Name
Δ binding imports

home-manager/profiles/work.nix

Status Type Name
Δ binding imports

secrets/personal/hackmd-api-token.age

Status Type Name
+ chunk lines 1-9

secrets/secrets.nix

Status Type Name
+ orphan module-level
+ binding work/hackmd-api-token.age.publicKeys
+ binding personal/hackmd-api-token.age.publicKeys

secrets/work/hackmd-api-token.age

Status Type Name
+ chunk lines 1-9

Summary: 33 added, 2 modified across 9 files (10 added orphans)

functions and classes, not lines · sem

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants