From fec40208281589b70228802914e98b3b4823924b Mon Sep 17 00:00:00 2001 From: Igor Soarez Date: Fri, 18 Sep 2026 13:03:16 +0100 Subject: link/claude: secrets in MCP specs, and a separate spec for Desktop Two things a spec in ~/.config/claude/mcp-servers.d/ may now carry beyond what Claude Code takes, both resolved at link time: - "${szecret:NAME}" in any string value, replaced with `szecret get NAME` when the server is registered with Claude Code. For a remote server's Authorization header: Claude Code's own headersHelper is not run in a workspace without persisted trust, which includes $HOME, so a token fetched that way is simply missing. A secret that cannot be read skips that server, loudly. - "desktop", a whole spec given to Claude Desktop instead. Desktop only starts local stdio servers, so a layer that moves a server to HTTP for Claude Code can keep Desktop on a local copy. --- link/claude.sh | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/link/claude.sh b/link/claude.sh index b7b3f9e..56f3724 100755 --- a/link/claude.sh +++ b/link/claude.sh @@ -31,6 +31,21 @@ link "$config_dir/claude/CLAUDE.md" "$HOME/.claude/CLAUDE.md" # # Run by link.sh after other layers' link.sh, so their files are in place on a # first link. +# +# Two things a spec may carry beyond what Claude Code takes, both resolved here: +# +# "${szecret:NAME}" anywhere in a string value, replaced with `szecret get +# NAME` as the server is registered with Claude Code. For a +# remote server's Authorization header: Claude Code's own +# headersHelper is not run in a workspace without persisted +# trust, which includes $HOME, so a token fetched there +# would simply be missing. The secret ends up in +# ~/.claude.json, which Claude Code keeps mode 600. A +# secret that cannot be read skips that server, loudly. +# +# "desktop" a whole spec to give Claude Desktop instead of this one, +# e.g. a local stdio server where Claude Code gets a remote +# one. Claude Code never sees it. mcp_dir="$xdg_config_dir/claude/mcp-servers.d" mcp_manifest="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles/claude-mcp-servers" claude_desktop_dir="$HOME/Library/Application Support/Claude" @@ -105,9 +120,34 @@ then then echo "Will register MCP server (user scope): $name" else + if ! code_spec="$(python3 -c 'import json, re, subprocess, sys +spec = json.loads(sys.argv[1]) +spec.pop("desktop", None) + +def secret(match): + name = match.group(1) + try: + return subprocess.run(["szecret", "get", name], check=True, capture_output=True, text=True).stdout.strip() + except (OSError, subprocess.CalledProcessError): + sys.exit("szecret get %s failed" % name) + +def expand(value): + if isinstance(value, str): + return re.sub(r"\$\{szecret:([A-Za-z0-9._-]+)\}", secret, value) + if isinstance(value, dict): + return {k: expand(v) for k, v in value.items()} + if isinstance(value, list): + return [expand(v) for v in value] + return value + +print(json.dumps(expand(spec)))' "$spec" 2>&1)" + then + red "MCP server (user scope) not registered: $name: $code_spec" + continue + fi green "MCP server (user scope): $name" claude mcp remove --scope user "$name" >/dev/null 2>&1 || true - claude mcp add-json --scope user "$name" "$spec" >/dev/null + claude mcp add-json --scope user "$name" "$code_spec" >/dev/null fi done <<< "$mcp_wanted" fi @@ -149,6 +189,7 @@ for line in os.environ["MCP_WANTED"].splitlines(): continue name, spec = line.split("\t", 1) spec = json.loads(spec) + spec = spec.get("desktop", spec) command = spec.get("command") if command: if not os.path.isabs(command): -- cgit v1.2.3