TypeScript
Set a GitHub Actions secret the right way
You cannot POST a plaintext Actions secret. You fetch the repo public key, seal the value with libsodium, then PUT the ciphertext.
23 Jun 2026
Three steps: get the key, seal, put. The seal is the part people skip and then wonder why the API rejects them.
import sodium from "libsodium-wrappers";
interface PublicKey {
key: string; // base64
key_id: string;
}
export async function setSecret(opts: {
owner: string;
repo: string;
name: string;
value: string;
token: string;
}): Promise<void> {
const api = `https://api.github.com/repos/${opts.owner}/${opts.repo}/actions/secrets`;
const headers = {
Authorization: `Bearer ${opts.token}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
};
const keyRes = await fetch(`${api}/public-key`, { headers });
if (!keyRes.ok) throw new Error(`public-key: ${keyRes.status}`);
const pk = (await keyRes.json()) as PublicKey;
await sodium.ready;
const sealed = sodium.crypto_box_seal(
sodium.from_string(opts.value),
sodium.from_base64(pk.key, sodium.base64_variants.ORIGINAL),
);
const encrypted_value = sodium.to_base64(
sealed,
sodium.base64_variants.ORIGINAL,
);
const putRes = await fetch(`${api}/${opts.name}`, {
method: "PUT",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ encrypted_value, key_id: pk.key_id }),
});
if (!putRes.ok && putRes.status !== 201 && putRes.status !== 204) {
throw new Error(`put secret: ${putRes.status}`);
}
}Gotchas
You cannot just send the plaintext. GitHub stores secrets sealed against a repo-specific public key, so the value has to be encrypted with crypto_box_seal and that exact key before the PUT. Send key_id from the same response, or the server cannot pick the matching private key to decrypt.
- A Dokku CHECKS file for zero-downtime deploysDokku swaps to a new container before it is actually ready, so requests hit a half-booted app. A CHECKS file makes Dokku wait for a real healthcheck to pass.Snippet
- Wrap a CLI as an MCP tool when there is no APIThe thing you want an agent to drive only has a CLI. execFile it, hand back stdout, and put bounds on the call so it cannot eat your process.Snippet
- mcp-dokkuAn MCP server that drives a Dokku PaaS over SSH.Tool
- Guardrails for agents in productionA catalogue of the guards I actually ship: typed confirmations, blast-radius escalation, pay-to-play gating, ordered workflows, and read-only by default.Musing
- mcp-search-consoleAn MCP server for Google Search Console properties, sitemaps, and analytics.Tool