fix: Retry the codespace skills install and add the security plugin (no-changelog) (#36764)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Declan Carroll
2026-08-21 14:48:54 +00:00
committed by GitHub
parent 047229449a
commit 14cd525465
4 changed files with 110 additions and 21 deletions
+43 -8
View File
@@ -127,12 +127,22 @@ and repo investigation. Login registers it from the repo-level
it. Tell the agent to call `get_flaky_context` first — it returns the rules
the tools assume.
## Quality skills (Claude plugin)
## Quality and security skills (Claude plugins)
Claude sessions can also load the private quality skills from the
`n8n-io/n8n-agent-skills` repository (bug insights, defect attribution,
mutation testing, and more). `post-start.mjs` installs the `quality` plugin on each
container start, so every session gets the skills with no per-session step.
Claude sessions can also load the private skills from the
`n8n-io/n8n-agent-skills` repository. `post-start.mjs` installs both plugins on
each container start, so every session gets the skills with no per-session step:
- `quality` — bug insights, defect attribution, flaky test investigation,
mutation and property testing, PR council, and more.
- `security` — security code review, adversarial review of security-fix PRs,
regression test generation, and Security Hub report triage.
Together they add roughly 5k always-on tokens to every session. Drop a plugin
from `PLUGINS` in `plugins.mjs` if that budget matters more than the skills.
`post-start.mjs` and the `pnpm session` prelude in `scripts/cloud-session.mjs`
both read that list, so a session that races the container start still gets
every plugin.
The private marketplace uses the codespace's own GitHub auth — no extra token.
`devcontainer.json` grants the codespace read access to
@@ -147,9 +157,34 @@ clones `owner/repo` shorthand over SSH and the private clone fails.
If a user does not authorize the grant, the clone fails and the skills step is
skipped (the worker still starts). Existing codespaces created before this change
need a recreate to get the prompt. The log is at `/tmp/post-start.log`: a failed
`skills repo reachable` line means the grant was not authorized; a failed
`marketplace add` after a reachable repo means a loader-auth problem.
need a recreate to get the prompt.
### When the skills are missing
`/tmp/post-start-status.json` lists what installed and what did not, and
`/tmp/post-start.log` has the detail. Reading the log:
- A failed `skills repo reachable` line means the grant was not authorized.
- A `marketplace add` failure mentioning `File exists` is a clone that died
partway through `~/.claude/plugins/marketplaces/n8n-io-n8n-agent-skills`, the
path the loader stages into before renaming it to the cache. This has been
seen once as a transient failure, so the script removes that path and retries
the add once.
- Any other `marketplace add` failure after a reachable repo means a
loader-auth problem.
A failure that survives the retry needs a human — the container still starts and
the worker still runs, only the skills are missing.
Both `marketplace add` and `plugin install` are idempotent, so re-running the
script by hand is safe:
```bash
node /workspaces/n8n/.devcontainer/codespaces/post-start.mjs
```
Verify with `claude plugin list`, then restart the session (or `/reload-plugins`)
to pull the skills into context.
## Viewing the dev UI locally
+5
View File
@@ -0,0 +1,5 @@
// Shared by post-start.mjs (installs at container start) and scripts/cloud-session.mjs
// (re-installs in the session prelude). Both must agree or a session boots with a
// partial skill set.
export const MARKETPLACE = 'n8n-io/n8n-agent-skills';
export const PLUGINS = ['quality@n8n-agent-skills', 'security@n8n-agent-skills'];
+49 -4
View File
@@ -1,9 +1,21 @@
#!/usr/bin/env node
// Runs on each codespace start. Installs the skills marketplace, starts the worker.
import { execFileSync } from 'node:child_process';
import { rmSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
import { MARKETPLACE, PLUGINS } from './plugins.mjs';
const MARKETPLACE = 'n8n-io/n8n-agent-skills';
const PLUGIN = 'quality@n8n-agent-skills';
const STATUS_FILE = '/tmp/post-start-status.json';
// Claude Code stages the marketplace clone here before renaming it to the cache dir.
// A clone that dies partway has been seen to leave the staging path in a state that
// fails the next clone ("cannot copy ... File exists"), so clear it before retrying.
const MARKETPLACE_STAGING = join(
homedir(),
'.claude/plugins/marketplaces',
MARKETPLACE.replace('/', '-'),
);
// The codespace clones over HTTPS and has no SSH key. The loader defaults to SSH
// for "owner/repo", so it must prefer HTTPS or the private clone fails.
@@ -20,10 +32,43 @@ function tryRun(label, cmd, args) {
}
}
// Both commands are idempotent and exit 0 when the marketplace or plugin is already
// present, so a healthy start re-runs them cheaply and only a real failure retries.
function addMarketplace() {
const add = (label) => tryRun(label, 'claude', ['plugin', 'marketplace', 'add', MARKETPLACE]);
if (add('marketplace add')) return true;
console.error(`marketplace add: clearing ${MARKETPLACE_STAGING} and retrying once`);
try {
rmSync(MARKETPLACE_STAGING, { recursive: true, force: true });
} catch (error) {
console.error(`marketplace staging cleanup: ${error.message}`);
return false;
}
return add('marketplace add (retry)');
}
tryRun('skills repo reachable', 'git', ['ls-remote', `https://github.com/${MARKETPLACE}`, 'HEAD']);
if (tryRun('marketplace add', 'claude', ['plugin', 'marketplace', 'add', MARKETPLACE]))
tryRun('plugin install', 'claude', ['plugin', 'install', PLUGIN]);
const installed = [];
const failed = [];
if (addMarketplace()) {
for (const plugin of PLUGINS) {
const ok = tryRun(`plugin install ${plugin}`, 'claude', ['plugin', 'install', plugin]);
(ok ? installed : failed).push(plugin);
}
} else {
failed.push(...PLUGINS);
}
writeFileSync(STATUS_FILE, JSON.stringify({ installed, failed }, null, 2));
// A skipped install is otherwise invisible until someone misses a skill mid-session.
if (failed.length > 0) {
console.error(`\n!! SKILLS NOT INSTALLED: ${failed.join(', ')}`);
console.error('!! Sessions start without them. Retry with:');
console.error('!! node /workspaces/n8n/.devcontainer/codespaces/post-start.mjs\n');
}
tryRun('worker start', 'tmux', [
'new-session',
+13 -9
View File
@@ -11,6 +11,8 @@
// pnpm session rm delete the codespace
import { execFileSync, spawnSync } from 'node:child_process';
import { MARKETPLACE, PLUGINS } from '../.devcontainer/codespaces/plugins.mjs';
const REPO = 'n8n-io/n8n';
const DEVCONTAINER = '.devcontainer/codespaces/devcontainer.json';
const MACHINE = 'premiumLinux'; // 8-core/32GB — the only size we use
@@ -73,26 +75,28 @@ const SECRETS = '. /usr/local/lib/codespaces-env.sh 2>/dev/null || true';
// Worktrees share the pnpm store but not the turbo cache; a shared TURBO_CACHE_DIR
// (seeded from the main checkout) keeps new-worktree builds at cache-hit speed.
const CACHE = 'export TURBO_CACHE_DIR=/workspaces/.turbo-cache; [ -d "$TURBO_CACHE_DIR" ] || cp -r /workspaces/n8n/.turbo/cache "$TURBO_CACHE_DIR" 2>/dev/null || mkdir -p "$TURBO_CACHE_DIR"';
// On a freshly created codespace, post-start.mjs installs the skills plugin via a
// On a freshly created codespace, post-start.mjs installs the skills plugins via a
// network clone that takes tens of seconds. If `claude` boots first it builds its
// skill registry before the plugin exists on disk, and /reload-plugins can't
// recover it in-process — so the first session silently loses every quality skill.
// Run the idempotent install here to block until the plugin is on disk (a fast
// skill registry before a plugin exists on disk, and /reload-plugins can't
// recover it in-process — so the first session silently loses those skills.
// Run the idempotent installs here to block until every plugin is on disk (a fast
// no-op once cached). Mirrors the env vars post-start.mjs sets for the private
// HTTPS clone; failures are tolerated so a plugin hiccup never blocks the session.
const MARKETPLACE = 'n8n-io/n8n-agent-skills';
const PLUGIN = 'quality@n8n-agent-skills';
const ENSURE_PLUGIN = `export CLAUDE_CODE_PLUGIN_PREFER_HTTPS=1 CLAUDE_CODE_PLUGIN_KEEP_MARKETPLACE_ON_FAILURE=1; claude plugin marketplace add ${MARKETPLACE} >/dev/null 2>&1 || true; claude plugin install ${PLUGIN} >/dev/null 2>&1 || true`;
const ENSURE_PLUGINS = [
'export CLAUDE_CODE_PLUGIN_PREFER_HTTPS=1 CLAUDE_CODE_PLUGIN_KEEP_MARKETPLACE_ON_FAILURE=1',
`claude plugin marketplace add ${MARKETPLACE} >/dev/null 2>&1 || true`,
...PLUGINS.map((plugin) => `claude plugin install ${plugin} >/dev/null 2>&1 || true`),
].join('; ');
function remoteCommand(session, extraArgs) {
const claude = `claude ${extraArgs}`.trim();
if (session === 'agent') return `${SECRETS}; ${CACHE}; ${ENSURE_PLUGIN}; cd /workspaces/n8n && ${claude}`;
if (session === 'agent') return `${SECRETS}; ${CACHE}; ${ENSURE_PLUGINS}; cd /workspaces/n8n && ${claude}`;
const wt = `/workspaces/wt-${session}`;
const branch = `session/${session}`;
return [
SECRETS,
CACHE,
ENSURE_PLUGIN,
ENSURE_PLUGINS,
`if [ ! -d "${wt}" ]; then echo "Setting up worktree ${wt}…"`,
`git -C /workspaces/n8n worktree add "${wt}" -b "${branch}" 2>/dev/null || git -C /workspaces/n8n worktree add "${wt}" "${branch}"`,
`(cd "${wt}" && pnpm install); fi`,