Skip to content

feat: provision Claude Code cloud sessions from source control - #1763

Merged
kyle-sexton merged 5 commits into
mainfrom
claude/code-cloud-sessions-research-wu7wzn
Jul 30, 2026
Merged

feat: provision Claude Code cloud sessions from source control#1763
kyle-sexton merged 5 commits into
mainfrom
claude/code-cloud-sessions-research-wu7wzn

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

No linked issue

Summary

Cloud environments are account-scoped, not per-repository, so everything repo-specific for Claude Code cloud sessions moves into source control: a SessionStart bootstrap hook, the repo declared as its own plugin marketplace with a curated default plugin set, and a research-backed how-to doc. The environment dialog stays generic (Default / Trusted / no variables / optional gh one-liner).

Fix

  • .claude/hooks/session-start.sh — cloud-only bootstrap (CLAUDE_CODE_REMOTE guard) that provisions a fresh cloud VM to the same tool inventory ci.yml pins, reading in-repo manifests where one exists: Node from .node-version via the VM's nvm, the pinned claude CLI + Biome via root npm ci, ruff from the hash-locked .github/requirements-ci.txt, best-effort pinned hygiene binaries (shellcheck, actionlint, typos, editorconfig-checker, gitleaks, shfmt, markdownlint-cli2, check-jsonschema), full git history for the base-ref diff gates, and session PATH persistence via $CLAUDE_ENV_FILE. Required steps fail the session start; best-effort steps warn and continue. Idempotent: ~40 s on a fresh VM, ~3 s on re-run.
  • .claude/settings.json — registers the hook (startup|resume), declares this repo as its own marketplace (extraKnownMarketplaces, directory source with a relative path), and enables a lean default plugin set (enabledPlugins): markdown-format, bash-format, biome-format, typos-format, actionlint, eol-normalizer, guardrails, source-control, skill-quality.
  • docs/CLOUD-SESSIONS.md — concepts → generic setup guide (pointers to official docs, no copied detail) → this repo's setup, including setup-script-vs-SessionStart decision criteria, the cache-the-hook performance lever, one-vs-several environment guidance, and the GitHub-MCP-vs-gh-CLI split.

Verification

  • Hook executed end-to-end on a real cloud VM: all tools land at exact CI pins (Node 24.18.0, ruff 0.15.22, shellcheck 0.11.0, actionlint 1.7.12, markdownlint-cli2 0.23.1, shfmt 3.12.0); idempotent re-run 2.7 s; exit 0.
  • .claude/settings.json validates against the official claude-code-settings JSON Schema; all nine plugin names verified present in .claude-plugin/marketplace.json.
  • Repo gates on changed files: shellcheck (repo rcfile), shfmt, typos, editorconfig-checker, markdownlint-cli2, check-shell-portability, check-skill-portability, gitleaks (origin/main..HEAD) — all pass.
  • Contract suites that previously SKIPped now run: plugins/bash-format 35/35, lib/hook-utils 115/115.
  • Not testable pre-merge: the marketplace/plugin auto-install at cloud session start — verify in the first cloud session after merge.

Related

N/A

🤖 Generated with Claude Code

https://claude.ai/code/session_01SxpZePLupZiV8E5bwFbmBS


Generated by Claude Code

claude added 4 commits July 30, 2026 01:16
Cloud environments are account-scoped, not per-repository, so everything
repo-specific moves into source control: .claude/settings.json registers a
SessionStart hook (startup|resume) and .claude/hooks/session-start.sh
provisions a fresh cloud VM to the same tool inventory ci.yml pins — Node
from .node-version, the claude CLI and Biome via root npm ci, ruff from the
hash-locked requirements file, and best-effort hygiene binaries (shellcheck,
actionlint, typos, editorconfig-checker, gitleaks, markdownlint-cli2,
check-jsonschema) plus full git history for the base-ref diff gates. Local
sessions exit immediately via the CLAUDE_CODE_REMOTE guard.

docs/CLOUD-SESSIONS.md records the research: environment scope, the
setup-script vs SessionStart split, recommended environment dialog settings,
and the standards-sync caveat for .claude/settings.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxpZePLupZiV8E5bwFbmBS
…c claims

Reshape docs/CLOUD-SESSIONS.md into three parts — what cloud sessions and
environments are, how to set them up for any account or repo (pointers to
the official pages instead of restated details, to avoid drift), and how
this repository is set up.

Correct an over-inferred claim: per the sync-commit history,
.claude/settings.json and .github/requirements-ci.txt are repo-owned, not
materialized from melodic-software/standards; of the files the hook reads,
only .node-version is in the synced set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxpZePLupZiV8E5bwFbmBS
…sions

Declare this repo as its own plugin marketplace (extraKnownMarketplaces,
directory source with a relative path) and enable a lean default set in
enabledPlugins: the six format/lint-on-edit hooks (markdown-format,
bash-format, biome-format, typos-format, actionlint, eol-normalizer) plus
guardrails, source-control, and skill-quality. Cloud sessions install these
at session start from the clone; local collaborators are prompted on folder
trust. Add shfmt (pinned, best-effort) to the session-start hook so the
bash-format plugin's format pass has its tool, and document the plugin set
and the GitHub-MCP-vs-gh-CLI split in docs/CLOUD-SESSIONS.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxpZePLupZiV8E5bwFbmBS
…ent strategy

Codify where setup belongs — cached VM setup script vs per-session
SessionStart hook — with the caching cost model, the guarded setup-script
line that bakes the repo bootstrap into the environment snapshot, and
one-vs-several environment guidance (e.g. a future .NET repo).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxpZePLupZiV8E5bwFbmBS
@claude

claude Bot commented Jul 30, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 6s —— View job


PR Review: feat: provision Claude Code cloud sessions from source control

Todo list:

  • Gather context and understand the request
  • Read changed files
  • Review .claude/hooks/session-start.sh
  • Review .claude/settings.json
  • Review docs/CLOUD-SESSIONS.md
  • Check alignment with CLAUDE.md guidelines
  • Post final review findings

Overall this is a well-thought-out, carefully layered addition. The architecture is sound (account-scoped environment stays generic, all repo-specific provisioning stays in source control), the idempotency design is good, the required/best-effort split is well-reasoned, and the documentation is thorough without duplicating upstream content. A couple of correctness and security items worth addressing:


$CLAUDE_ENV_FILE append is not idempotent on resume

The hook is documented as idempotent, but the PATH persistence block appends unconditionally on every startup|resume event:

.claude/hooks/session-start.sh L69–73

if [[ -n "${CLAUDE_ENV_FILE:-}" ]]; then
  printf 'export PATH="%s:%s/node_modules/.bin:%s:$PATH"\n' \
    "$node_bin" "$repo_root" "$bin_dir" >>"$CLAUDE_ENV_FILE"
fi

If $CLAUDE_ENV_FILE is the same file across multiple resumes within a session (the docs don't appear to guarantee it's reset per invocation), every resume appends another PATH entry. Duplicate entries are harmless to bash evaluation, but they grow without bound and contradict the stated idempotency. A guard that checks for the entry before appending would make this truly idempotent:

if [[ -n "${CLAUDE_ENV_FILE:-}" ]] && ! grep -qF "node_modules/.bin" "$CLAUDE_ENV_FILE" 2>/dev/null; then
  printf 'export PATH="%s:%s/node_modules/.bin:%s:$PATH"\n' \
    "$node_bin" "$repo_root" "$bin_dir" >>"$CLAUDE_ENV_FILE"
fi

Fix this →


Binary downloads without integrity verification

fetch_release_tool downloads GitHub release tarballs with curl -fsSL but verifies neither checksums nor signatures:

.claude/hooks/session-start.sh L103–108

case "$url" in
*.tar.xz) curl -fsSL "$url" | tar -xJ -C "$tmp" || ok=0 ;;
*) curl -fsSL "$url" | tar -xz -C "$tmp" || ok=0 ;;
esac

Version-pinned URLs reduce the risk (you can't be redirected to a later release), and the PR correctly marks all GitHub release installs as best-effort given the cloud proxy constraints. But a MITM or compromised GitHub release would install a binary that runs with the session's full tool access. The mitigating factors here — ephemeral VM, private repo, cloud proxy constraints — make the risk low, but it's worth a conscious decision rather than a silent gap.

Two options for future hardening: (1) add sha256sum -c checks against expected hashes hardcoded alongside the version pins, or (2) prefer tools available via uv tool install or pip install --require-hashes (which ruff already uses) where possible, since those have integrity built in.

The shfmt direct-binary download has the same pattern (L143–152).


Minor: ok variable convention is inverted vs shell semantics

Inside fetch_release_tool, ok=1 means "success" and ok=0 means "failure":

.claude/hooks/session-start.sh L97–99

local ok=1
case "$url" in
*.tar.xz) curl -fsSL "$url" | tar -xJ -C "$tmp" || ok=0 ;;

This is the opposite of shell exit-code convention (0 = success, non-zero = failure), which makes [[ "$ok" -eq 1 && -f "$tmp/$member" ]] a small cognitive bump when reading. local failed=0 / failed=1 or using a boolean-named variable (local succeeded=1) would align intent with shell idiom.


settings.json — hook command quoting is correct

The quoting in "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh" correctly handles project directories with spaces. No issue.


docs/CLOUD-SESSIONS.md — link resolution is correct

The relative link [upstream-drift convention](conventions/upstream-drift/README.md) from docs/CLOUD-SESSIONS.md resolves to docs/conventions/upstream-drift/README.md, which exists. Verified.


Overall

The design decisions are sound and well-documented: the CLAUDE_CODE_REMOTE guard preventing local mutation, reading version pins from in-repo manifests rather than duplicating them, the required/best-effort split with the reasoning for each tool, and the marketplace + enabledPlugins wiring in settings.json. The one correctness item (PATH dedup on resume) is worth fixing before merge. The binary integrity note is lower priority but worth a decision.

@claude

claude Bot commented Jul 30, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 48s —— View job


Security Review

  • Gather context and read PR changes
  • Review .claude/hooks/session-start.sh for injection, credential, and execution safety issues
  • Review .claude/settings.json for trust/permission issues
  • Review docs/CLOUD-SESSIONS.md for security guidance accuracy
  • Report findings

Two findings. No injection surfaces, no credential exposure, no dangerous GitHub Actions patterns. The issues are both in binary supply-chain trust.


IMPORTANT — Binary tool downloads execute without integrity verification

Confidence: HIGH

fetch_release_tool downloads archives from GitHub release URLs and extracts them with:

curl -fsSL "$url" | tar -xz -C "$tmp" || ok=0

It then checks only that the expected file path exists in the extracted tree before installing it with install -m 0755. No SHA256, no BLAKE2, no sigstore verification — the only trust signal is HTTPS delivery from the URL. The same absence of integrity checking applies to the bare-binary shfmt download:

curl -fsSL -o "$bin_dir/shfmt" \
  "https://github.com/mvdan/sh/releases/download/${shfmt_pin}/shfmt_${shfmt_pin}_linux_amd64"
chmod 0755 "$bin_dir/shfmt"

This is notably inconsistent: ruff — on the same script, a few lines earlier — is installed via --require-hashes from a hash-locked requirements file, showing the team has already reasoned about this threat model. The five tools installed via fetch_release_tool (shellcheck, actionlint, typos, editorconfig-checker, gitleaks) plus shfmt get no equivalent protection.

Exploitation scenario: If any of the six upstream GitHub release pages serves a tampered binary (supply-chain compromise of those repos' release infrastructure), or if a MITM occurs between the cloud VM and GitHub's release CDN, a malicious binary lands in ~/.local/bin, is prepended to $PATH, and executes with the session user's full privileges whenever the agent invokes it. The PR description itself notes that the cloud proxy blocks GitHub API calls and /releases/latest redirects, implying the download path goes through the proxy — so the proxy's trustworthiness is also in the blast radius.

Blast radius: Limited to a single ephemeral cloud session (no persistent storage, no repo write access beyond what the GitHub proxy grants). However, a compromised gitleaks binary invoked during a secret-scanning step would have access to the entire repository checkout including any secrets present.

Recommendation: Add SHA256 verification to fetch_release_tool after extraction (and for shfmt after download), using hashes pinned in the same block as the version pins. GitHub publishes checksums.txt or equivalent alongside most of these releases, or hashes can be committed directly. Example pattern:

# in VERSION PINS block
shellcheck_sha256="<hash>"

# in fetch_release_tool or inline
echo "$expected_sha256  $bin_dir/$name" | sha256sum --check --quiet || {
  rm -f "$bin_dir/$name"
  echo "session-start: warning: $name checksum mismatch; not installed" >&2
  return 0
}

Fix this →


SUGGESTION — check-jsonschema installed without a version pin

Confidence: HIGH

The check-jsonschema install paths both lack a version constraint:

uv tool install --quiet check-jsonschema    # resolves "latest"
# or fallback:
python3 -m pip install --user --quiet check-jsonschema   # also "latest"

See lines ~158–164. Every other tool in this script is version-pinned (either from an in-repo manifest or the VERSION PINS block); this is the only exception. A future check-jsonschema release that introduces a breaking CLI flag or schema-validation behavior change would silently alter the gate's output without any pin bump in this file to review.

Recommendation: Add check_jsonschema_pin to the VERSION PINS block and pin both install paths:

check_jsonschema_pin="0.33.0"   # or whatever the current version is
uv tool install --quiet "check-jsonschema==${check_jsonschema_pin}"
# or:
python3 -m pip install --user --quiet "check-jsonschema==${check_jsonschema_pin}"

Fix this →


No other security issues found

  • Shell injection: node_pin (read from .node-version) and all other repo-manifest-sourced values are properly double-quoted throughout; no eval, no unquoted expansions, no $() in unsanitized contexts.
  • CLAUDE_ENV_FILE write: The printf appends a well-formed export PATH=... line; all three interpolated values ($node_bin, $repo_root, $bin_dir) derive from harness-provided or script-computed paths, not external input.
  • CLAUDE_CODE_REMOTE guard: Relies on a harness-set environment variable to skip local execution. A local developer who manually sets CLAUDE_CODE_REMOTE=true would trigger tool installs, but the script performs no destructive local mutation and the risk is self-inflicted.
  • settings.json hook command: bash "$CLAUDE_PROJECT_DIR"/.claude/hooks/session-start.sh follows the documented pattern; CLAUDE_PROJECT_DIR is harness-provided, not user-controlled input.
  • extraKnownMarketplaces / enabledPlugins: The directory source resolves to the repo's own checked-in manifests — consistent with the documented plugin trust model and appropriate for a marketplace repo loading its own plugins.
  • docs/CLOUD-SESSIONS.md: Security-relevant guidance (env vars are not a secrets store; CLAUDE_CODE_REMOTE guard; idempotency requirements; proxy behavior) is accurate and well-scoped.

@kyle-sexton
kyle-sexton enabled auto-merge (squash) July 30, 2026 01:49

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 67ee12f7f9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .claude/hooks/session-start.sh Outdated
Address all four reviewer findings on #1763:
- Verify a pinned SHA-256 for every GitHub-release asset before install
  (shellcheck, actionlint, typos, editorconfig-checker, gitleaks, shfmt);
  a mismatch refuses the install with a visible warning and the session
  continues. shfmt now flows through the same fetch_release_tool path as
  a bare-binary asset.
- Deduplicate the CLAUDE_ENV_FILE PATH append so resume re-runs cannot
  stack duplicate lines (exact-line grep guard).
- Fetch origin/main with an explicit destination refspec so the ref
  materializes in single-branch clones where a bare fetch only writes
  FETCH_HEAD.
- Pin check-jsonschema, the one previously unpinned install.
Also rename the inverted ok flag to failed for shell-idiomatic reading.

Verified: fresh-download run installs all six tools at pinned versions
through the checksum path; a corrupted pin refuses install and exits 0;
the env file holds exactly one PATH line after two runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxpZePLupZiV8E5bwFbmBS

Copy link
Copy Markdown
Contributor Author

All four reviewer findings addressed in 612510d:

  • SHA-256 verification (security review, code review): every GitHub-release asset now carries a pinned hash verified before install; mismatch refuses the install with a visible warning and the session continues. shfmt flows through the same path as a bare-binary asset. Negative-tested: a corrupted pin produces checksum mismatch … refusing to install and the tool reports ABSENT. Hashes were recorded from verified downloads of the exact pinned versions; pin and hash bump together.
  • CLAUDE_ENV_FILE dedup (code review): exact-line grep guard; verified the file holds one PATH line after two consecutive runs.
  • origin/main refspec (Codex): now fetch origin "+main:refs/remotes/origin/main" so the ref materializes in single-branch clones — correct catch, a bare fetch origin main only writes FETCH_HEAD there.
  • check-jsonschema pin (security review): pinned to 0.37.4.

Also renamed the inverted ok flag to failed per the style note.


Generated by Claude Code

@claude

claude Bot commented Jul 30, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 25s —— View job


PR Review: feat: provision Claude Code cloud sessions from source control

Todo list:

  • Gather context and understand the request
  • Read the updated changed files (post-612510dd)
  • Review .claude/hooks/session-start.sh — verify all four prior findings are correctly addressed
  • Review .claude/settings.json
  • Review docs/CLOUD-SESSIONS.md
  • Post final review findings

This review covers the current HEAD (612510dd) — verifying that all four findings from the prior code and security reviews are correctly resolved and checking for any new issues.

All four prior findings: correctly fixed

SHA-256 verification (fetch_release_tool) — The function now downloads to $tmp/asset first, runs sha256sum --check --quiet --status before extraction, and refuses install on mismatch with a visible warning. shfmt flows through the same path via the *) member="asset" case, eliminating the separate code path. The hash format ("$sha $tmp/asset") is correct sha256sum input, and --status + ! inversion works cleanly under set -e. ✅

CLAUDE_ENV_FILE dedup (L72–80) — Uses grep -qxF "$path_line" (exact full-line match via -x) rather than the partial-string suggestion from the prior review. Strictly correct: a line with different paths would produce a different full string and would not suppress a legitimate new append. ✅

origin/main refspec (L196–197) — Now git fetch --quiet origin "+main:refs/remotes/origin/main". The + (force-update) correctly handles divergence in the remote ref. ✅

check-jsonschema pin (L53) + okfailed (L100) — Both cleanly applied. ✅


New observations (no blockers)

markdownlint-cli2 npm install has no SHA check (L167–169)

npm install -g --no-audit --no-fund "markdownlint-cli2@${markdownlint_pin}" || ...

Unlike the six GitHub-release tools (which now verify SHA-256) and ruff (--require-hashes), markdownlint-cli2 is installed from npm with no integrity check beyond the registry's TLS. The check_jsonschema_pin comment acknowledges "pip/uv installs carry registry integrity checks" — npm's content-addressable store provides similar assurance at the registry level, so this is an acceptable asymmetry. Worth an explicit comment noting the distinction if future reviewers ask.

SHA hash values are author-attested, not derivable from the script — The PR description says hashes were recorded from verified downloads. There's no sha256sum generation command documented alongside the VERSION PINS block. A one-line comment like # sha256sum <downloaded_file> would help the person doing the next pin bump. Very minor.

settings.json wiring — Hook registration, extraKnownMarketplaces, and enabledPlugins all look correct. The directory source with relative "./" path, the nine plugin names against .claude-plugin/marketplace.json, and the hook command quoting are consistent with the PR's stated verification.

docs/CLOUD-SESSIONS.md — Accurate, appropriately thin (links rather than copying upstream detail), and the decision-criteria and one-vs-several-environment sections are genuinely useful. The performance-lever snippet (CLAUDE_CODE_REMOTE=true bash .claude/hooks/session-start.sh) correctly demonstrates how to bake hook results into the environment cache. Link to conventions/upstream-drift/README.md resolves (verified from prior review).


Overall

The four prior findings are all correctly fixed. The implementation is sound: CLAUDE_CODE_REMOTE guard, SHA verification before any binary is installed, set -euo pipefail with proper required-vs-best-effort split, idempotent PATH dedup, and the corrected remote ref. No new blockers. The two observations above are informational.

@claude

claude Bot commented Jul 30, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 57s —— View job


Security Review

Todo list:

  • Gather context and understand the request
  • Read changed files at HEAD (612510dd)
  • Review .claude/hooks/session-start.sh for injection, credential, and execution safety issues
  • Review .claude/settings.json for trust/permission issues
  • Review docs/CLOUD-SESSIONS.md for security guidance accuracy
  • Report findings

The two IMPORTANT findings from the initial security review have been correctly addressed in 612510dd:

  • Binary integrity (fetch_release_tool): downloads now land in $tmp/asset, SHA-256 is verified before extraction or install, and the mismatch path refuses the install and warns. The shfmt bare-binary case flows through the same $tmp/asset path correctly (member="asset" in the case fallback branch, so $tmp/$member resolves to the verified file). Implementation is sound.
  • check-jsonschema pin: check_jsonschema_pin="0.37.4" is applied to both install paths (uv tool install and the pip fallback).

No CRITICAL or IMPORTANT findings on the updated code. Two low-severity items follow.


SUGGESTION — NVM_DIR sourced from environment without path validation

Confidence: MEDIUM

Lines 53–60:

export NVM_DIR="${NVM_DIR:-/opt/nvm}"
if [[ ! -s "$NVM_DIR/nvm.sh" ]]; then
    echo "session-start: error: Node $node_pin required and nvm not found at $NVM_DIR" >&2
    exit 1
fi
set +u
. "$NVM_DIR/nvm.sh"

The script sources "$NVM_DIR/nvm.sh" where NVM_DIR is taken verbatim from the process environment (with /opt/nvm as a fallback). If anything in the cloud VM's environment supplies a crafted NVM_DIR pointing to a directory with a malicious nvm.sh, the hook would execute it with the session's full privileges.

The CLAUDE_CODE_REMOTE guard prevents this from running locally. On Anthropic's cloud infrastructure the environment is controlled, making this practically very low risk. But the pattern — sourcing a path derived from an environment variable without validating it against a known-good allowlist — is worth flagging for completeness.

A simple hardening option is to prefer the expected path and treat a differing NVM_DIR as suspicious:

expected_nvm_dir="/opt/nvm"
if [[ "${NVM_DIR:-}" != "$expected_nvm_dir" && -n "${NVM_DIR:-}" ]]; then
  echo "session-start: warning: NVM_DIR is set to unexpected value '${NVM_DIR}'; using $expected_nvm_dir" >&2
fi
export NVM_DIR="$expected_nvm_dir"

SUGGESTION — markdownlint-cli2 global npm install uses implicit registry integrity, not explicit hash pinning

Confidence: HIGH (the inconsistency is real; exploitability is low)

Lines 167–170:

npm install -g --no-audit --no-fund "markdownlint-cli2@${markdownlint_pin}" ||
    echo "session-start: warning: markdownlint-cli2 install failed" >&2

npm global installs (-g) verify package integrity against the _integrity field in the registry metadata (sha512 of the tarball), so the install isn't unverified. But the trust model differs from the explicit SHA-256 approach the script now uses for GitHub release binaries: if the npm registry metadata itself were tampered (or if a hoisted package hijacks the name), the _integrity field would match the tampered artifact and pass. This contrasts with ruff's --require-hashes where hashes are committed in the repo and cannot be overwritten by a compromised registry response.

There is no direct equivalent to --require-hashes for npm install -g, so this is a structural limitation of npm's global install path rather than an oversight. Noting the asymmetry here so it's a conscious tradeoff rather than a gap.


No other security issues found

  • Shell injection: all interpolated values in $(), printf, and command arguments are double-quoted; node_pin from .node-version goes through tr -d '[:space:]' and is quoted in nvm install "$node_pin" — safe.
  • CLAUDE_ENV_FILE write: the grep -qxF guard prevents duplicate PATH lines on resume; the three values interpolated into the PATH line derive from harness-provided and system paths — no external input.
  • report_tool: calls "$name" "$@" where both are script-controlled literals — no injection surface.
  • Credential handling: no tokens or secrets written anywhere; docs/CLOUD-SESSIONS.md correctly documents that env vars are not a secrets store.
  • settings.json trust model: directory source with ./ resolves against the checked-out repo — appropriate for a marketplace repo loading its own plugins; enabledPlugins entries are all repo-committed and curated.
  • CLAUDE_CODE_REMOTE guard: correctly prevents local execution; the set -euo pipefail ensures failures in required steps propagate.

@kyle-sexton
kyle-sexton merged commit b6e086a into main Jul 30, 2026
31 checks passed
@kyle-sexton
kyle-sexton deleted the claude/code-cloud-sessions-research-wu7wzn branch July 30, 2026 02:29
kyle-sexton added a commit that referenced this pull request Jul 30, 2026
…#1773)

No linked issue

## Summary

Every first-attempt `pr-issue-linkage` CI failure in this repo is an
authoring-time knowledge gap: the agent writing the PR body has no way
to know the contract until CI rejects it one full round trip later
(#1766 showed the pattern today — red at creation, green after a body
edit). This PR makes the contract known and enforced at authoring time,
on every surface a PR body can be written through, without weakening the
required check itself.

## Fix

Three layers, repo-level and plugin-level:

- **CLAUDE.md** documents the body contract (closing keyword or literal
`No linked issue`, plus a non-empty `## Related` section, validated
after HTML-comment stripping) so agents write a passing body on the
first try.
- **Checked-in MCP gate** (`.claude/hooks/pr-linkage-mcp-gate.sh`, wired
in `.claude/settings.json`): a PreToolUse hook that blocks a GitHub-MCP
`create_pull_request`/`update_pull_request` whose body would fail the
check — the surface cloud sessions use — with the exact missing lines
named in the block message. Loads in any session that opens this repo,
no plugin required.
- **source-control plugin 0.41.0**: the same gate ships as
`pr-linkage-mcp-gate` for every marketplace consumer, sibling to the
existing `pr-body-linkage-gate` (`gh` surface); kill switch
`pr_linkage_mcp_gate_enabled` (default true). The validator core
(comment stripping, keyword/`## Related` judging, verdict wording) is
extracted to one sourced lib, `pr-linkage-validator.sh`, consumed by
both plugin hooks and the checked-in repo hook, so a drift fix against
the upstream ci-workflows validator lands on every surface atomically.

The merge with `main` also hand-reconciles `.claude/settings.json` where
git's textual merge of this branch and #1763 produced duplicate
`hooks`/`extraKnownMarketplaces`/`enabledPlugins` keys (JSON last-wins
would have silently dropped the SessionStart hook and the 9-plugin
roster): the file now carries both hooks and #1763's marketplace source
and plugin set.

## Verification

- `plugins/source-control/hooks/pr-linkage-mcp-gate.test.sh`: 17/17
(validator shapes, create-without-body blocks, update-without-body
allows, owner/repo scope guard, kill switch).
- `plugins/source-control/hooks/pr-body-linkage-gate.test.sh`: 135/135 —
sibling regression across the shared-lib extraction.
- `.claude/hooks/pr-linkage-mcp-gate.test.sh`: 16/16 against the
checked-in hook sourcing the lib.
- `shellcheck` (repo rcfile semantics) and `shfmt` clean on all four
shell files; `check-changelog-parity.sh --check-bump origin/main`,
`sync-hook-utils.sh --check`, and `check-manifest-duplicate-keys.py` all
pass.
- Hook contracts (PreToolUse MCP matchers, plugin `hooks/hooks.json`,
`userConfig` → `CLAUDE_PLUGIN_OPTION_*` mapping) verified against the
current hooks and plugins-reference docs fetched this session, per the
fresh-docs mandate.

## Related

- Refs #1763 — the cloud-session provisioning PR whose
`.claude/settings.json` this branch merges with and reconciles.
- Refs #1766 — today's observed instance of the first-attempt
`pr-issue-linkage` failure this PR prevents.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---
_Generated by [Claude
Code](https://claude.ai/code/session_01KJdyaSTPhgkjYwJnVnhdV4)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 30, 2026
…session-start hook (#1772)

No linked issue

## Summary

Comment-only follow-up closing out the two informational notes the
automated reviews left on #1763 after its blocking findings were fixed,
so they're recorded in the code instead of a merged PR thread.

## Fix

- Document the hash-bump procedure alongside the `VERSION PINS` block in
`.claude/hooks/session-start.sh`: download the new asset, confirm the
binary reports the pinned version, record `sha256sum
<downloaded-asset>`.
- State explicitly that the npm and pip/uv installs (markdownlint-cli2,
check-jsonschema) rely on registry-level integrity metadata — an
accepted, weaker trust anchor than the committed SHA-256s, since `npm
install -g` has no `--require-hashes` equivalent.
- The third low-severity review note (NVM_DIR path validation) is
deliberately declined, with the reasoning in the commit message: forcing
`/opt/nvm` risks breaking session start if the cloud image legitimately
relocates nvm, against a threat that already requires environment
control.

## Verification

Comment-only diff (7 insertions, 1 deletion, one file). shfmt,
shellcheck (repo rcfile), typos, and editorconfig-checker all pass on
the changed file; hook behavior unchanged.

## Related

Refs #1763

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01SxpZePLupZiV8E5bwFbmBS

---
_Generated by [Claude
Code](https://claude.ai/code/session_01SxpZePLupZiV8E5bwFbmBS)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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