Skip to content

fix(#6358): load sandbox hook settings via --settings flag - #6405

Open
fullsend-ai-coder[bot] wants to merge 3 commits into
mainfrom
agent/6358-hooks-settings-path
Open

fix(#6358): load sandbox hook settings via --settings flag#6405
fullsend-ai-coder[bot] wants to merge 3 commits into
mainfrom
agent/6358-hooks-settings-path

Conversation

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor

Summary

Sandbox tool hooks (Tirith, SSRF, canary, secret redaction, unicode normalization, context suppression, tool allowlist) were silently never loaded by Claude Code in production runs. installClaudeHooks wrote hook wiring to /sandbox/workspace/.claude/settings.json, but buildRunCommand starts the CLI from /sandbox/workspace/<repo>. Claude Code only reads project settings from <cwd>/.claude/settings.json — the hooks directory was a sibling, not a parent, so the settings were never found.

Changes

  • Move hooks to runner-owned config directory: Hook scripts now live at /sandbox/claude-config/hooks/ and settings at /sandbox/claude-config/hooks.json — co-located under the runner-owned directory, outside the agent-writable workspace tree
  • Pass settings via --settings flag: buildRunCommand appends --settings /sandbox/claude-config/hooks.json when HooksSettingsPath is set in RunParams, which run.go populates when security is enabled. --settings takes precedence over project/local settings
  • Golden tests: Three new tests verify --settings presence, absence, and quote escaping. The no-double-spaces table test updated to cover the new flag
  • Documentation: docs/runtimes.md workspace layout diagram updated to reflect the new paths

Testing

  • go test ./internal/runtime/... — all tests pass including new golden tests
  • go test ./internal/security/... — all tests pass
  • go vet ./... — clean
  • go build ./... — clean
  • buildRunCommand at 100% coverage
  • Pre-commit could not run (sandbox network policy); post-script runs authoritatively

Follow-up

Repo-owned <repo>/.claude/settings.json hooks are still loaded from <cwd> by Claude Code. The --settings flag ensures runner hooks take precedence, but a malicious repo could add its own hooks. A follow-up issue should assess whether to scan or disable repo-owned hooks.


Closes #6358

Post-script verification

  • Branch is not main/master (agent/6358-hooks-settings-path)
  • Secret scan passed (gitleaks — 2e78d663bb7aa15db58e97e83dcf9d27eacdc0ac..HEAD)
  • PR body secret scan passed (gitleaks — no-git)

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown

Site preview

Preview: https://97fe8990-site.fullsend-ai.workers.dev

Commit: 0a16c181356c41e2df37a0c5716287f58f11fa1e

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 20, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 10:57 AM UTC · Completed 11:15 AM UTC

Commit: 16432d1 · View workflow run →

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 71.42857% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/runtime/claude.go 71.42% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 20, 2026

Copy link
Copy Markdown

Review

Findings

Medium

  • [security-hook-bypass] internal/runtime/claude.go:339 — Repo-owned hooks can run alongside runner-owned security hooks, potentially interfering with them. A target repo can include its own .claude/settings.json with hooks definitions that Claude Code loads from <cwd>/.claude/settings.json as project-level settings. While --settings gives the runner hooks precedence, project-level hooks are additive — a malicious repo could install hooks that interfere with the security hook chain. This risk is pre-existing and not introduced by this PR; the PR strictly improves the status quo (hooks were silently never loaded before). The PR body acknowledges this and recommends a follow-up.
    Remediation: File a follow-up issue to evaluate whether repo-level hook definitions should be blocked or overwritten before the agent starts.

Low

  • [acceptance-criteria-gap] Issue Sandbox tool hooks are never loaded: settings.json is written to /sandbox/workspace/.claude but Claude Code runs from /sandbox/workspace/<repo> #6358 acceptance criterion docs: add agent infrastructure problem document #5 states "Assess the repo-owned <repo>/.claude/settings.json hooks exposure and file a follow-up if needed." The PR body assesses the risk and recommends a follow-up but does not reference a filed follow-up issue.

  • [stale-diagram] docs/guides/dev/cli-internals.md:424 — The sandbox execution diagram references buildClaudeCommand() but the actual function is buildRunCommand(). The diagram also omits the new conditional --settings flag added by this PR.
    Remediation: Update the function name and add --settings {hooksSettings} to the flag listing.

  • [fail-open] internal/cli/run.go:1495 — When h.SecurityEnabled() returns false, no security hooks are loaded. This is intentional and SecurityEnabled() defaults to true, so this is not a fail-open in the default case.

  • [naming-coherence] internal/security/hooks.go:47claudeSettings and GenerateClaudeSettings names reference "settings" while the output file is now hooks.json. The naming still works (it generates a Claude Code settings structure for --settings) but the mismatch may cause confusion.

Previous run

Review

Findings

Medium

  • [API contract] internal/runtime/claude.go — The fix relies on Claude Code's --settings flag merging additively with {CLAUDE_CONFIG_DIR}/settings.json (which stores enabledPlugins from bootstrapPlugins). If --settings replaces the user-level settings entirely, plugin loading would be silently disabled. No in-repo test covers the "plugins + hooks both active" scenario.
    Remediation: Verify Claude Code CLI --settings semantics and consider adding a test confirming plugins remain functional when --settings is also passed.

Low

  • [security control bypass] internal/runtime/claude.go:340 — Hook scripts and hooks.json are writable by the same sandbox UID that runs the agent. No chmod/chown post-upload. A compromised agent could overwrite hook scripts to disable security controls. The comment "outside the agent-writable workspace tree" overstates the filesystem-level protection. Not a regression — the old location had the same vulnerability.

  • [scope-vs-authorization] — Issue Sandbox tool hooks are never loaded: settings.json is written to /sandbox/workspace/.claude but Claude Code runs from /sandbox/workspace/<repo> #6358 acceptance criterion 5 requires filing a follow-up issue to assess repo-owned .claude/settings.json hooks exposure. The PR body acknowledges this but the follow-up issue does not appear to have been filed.

  • [stale-documentation] docs/guides/dev/cli-internals.md:424 — The command template does not include the new --settings flag added by this PR.

  • [stale-documentation] docs/guides/dev/cli-internals.md:401 — The bootstrap section lists security hooks as uploaded to /sandbox/workspace, but they now go to /sandbox/claude-config/hooks/.


Labels: PR modifies sandbox security hook installation (internal/security/hooks.go) and runner CLI/runtime (internal/cli/run.go, internal/runtime/)

Previous run (2)

Review

Findings

Medium

  • [stale-reference] internal/security/hooks.go:46 — Two stale comments remain after the path relocation: (a) the claudeSettings struct comment says "represents the .claude/settings.json structure" and (b) the GenerateClaudeSettings docstring says "produces a .claude/settings.json". Both now describe hooks.json content written to SandboxHooksSettings under the runner-owned config directory.
    Remediation: Update struct comment to "represents the hooks.json structure for Claude Code hook wiring" and the function docstring to "produces a hooks.json with security hooks..."

  • [stale-reference] internal/cli/run.go:1773 — Comment in bootstrapCommon says "Claude hook scripts live under workspace/.claude/" but this PR moves hook scripts to claude-config/hooks/ (SandboxClaudeConfig + "/hooks").
    Remediation: Update comment to reference claude-config/hooks/.

  • [scope-gap] Issue Sandbox tool hooks are never loaded: settings.json is written to /sandbox/workspace/.claude but Claude Code runs from /sandbox/workspace/<repo> #6358 acceptance criterion 3 requires an e2e/behaviour test or --debug hooks capture demonstrating hooks actually fire at runtime. The PR includes golden/unit tests for command string construction but no evidence that hooks load and execute in the sandbox.

Low

@fullsend-ai-review fullsend-ai-review Bot added the requires-manual-review Review requires human judgment label Aug 20, 2026
Comment thread internal/security/hooks.go Outdated
// Claude Code loads the hooks regardless of its working directory.
const SandboxHooksSettings = sandbox.SandboxClaudeConfig + "/hooks.json"

// GenerateClaudeSettings produces a .claude/settings.json with security hooks

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Low] Stale doc comments — claudeSettings (line 46) and GenerateClaudeSettings (line 61) still reference .claude/settings.json but the output is now hooks.json under SandboxHooksSettings.

@maruiz93

Copy link
Copy Markdown
Contributor

[High] The issue acceptance criteria require "a behaviour/e2e or functional check exercises one blocking hook end-to-end under the pinned CLI." The golden tests added here verify the --settings flag appears in the command string, but they wouldn't have caught the original bug — the wiring was correct, the location was wrong. A Gherkin behaviour test under e2e/behaviour/features/ that configures a blocking hook, runs an agent, and asserts the hook fires (N > 0 in "Found N total hooks in registry") would catch this class of "silently not loaded" regression.

Hook scripts and hooks.json are writable by the agent process after installation. Since Claude Code runs with --dangerously-skip-permissions, a prompt-injected agent could overwrite hook scripts between tool invocations, disabling security controls. chmod alone doesn't help — the agent runs as the same user. The real fix is landlock-based path protection, tracked in NVIDIA/OpenShell#1268 and #6404.

[Low] internal/cli/run.go:1773 — stale comment says hook scripts live under workspace/.claude/ but they now live under claude-config/hooks/.

@maruiz93

Copy link
Copy Markdown
Contributor

/fs-fix Address the review comments: add a Gherkin behaviour test, fix the stale comments in run.go:1773, hooks.go:46, and hooks.go:61.

@fullsend-ai-coder

fullsend-ai-coder Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Finished Fix · ✅ Success · Started 11:53 AM UTC · Completed 12:05 PM UTC

Commit: 16432d1 · View workflow run →

fullsend-ai-coder Bot added a commit that referenced this pull request Aug 20, 2026
- run.go:1773: update comment to reference claude-config/hooks/ instead
  of the old workspace/.claude/ path
- hooks.go:46: update claudeSettings comment to reference hooks.json
  instead of .claude/settings.json
- hooks.go:61: update GenerateClaudeSettings docstring to reference
  hooks.json instead of .claude/settings.json
- Add hooks-loaded.feature: Gherkin behaviour test that dispatches a
  custom harness, attempts to fetch a cloud metadata endpoint
  (169.254.169.254), and asserts the SSRF PreToolUse hook blocks it.
  Catches the "silently not loaded" regression class.

Addresses review feedback on #6405
@fullsend-ai-coder

Copy link
Copy Markdown
Contributor Author

🔧 Fix agent — iteration 1 (human-triggered)

Addressed all 4 review findings: fixed 3 stale comments referencing old hook paths (.claude/settings.json, workspace/.claude/) and added a Gherkin behaviour test (hooks-loaded.feature) that verifies the SSRF PreToolUse hook fires end-to-end when hooks are loaded via --settings.

Fixed (4):

  1. stale comment in run.go:1773 (internal/cli/run.go): Updated comment from 'workspace/.claude/' to 'claude-config/hooks/' to reflect the new hook script location after this PR's path relocation
  2. stale comment in hooks.go:46 (claudeSettings struct) (internal/security/hooks.go): Updated claudeSettings struct comment from 'represents the .claude/settings.json structure' to 'represents the hooks.json structure for Claude Code hook wiring'
  3. stale comment in hooks.go:61 (GenerateClaudeSettings docstring) (internal/security/hooks.go): Updated GenerateClaudeSettings docstring from 'produces a .claude/settings.json' to 'produces a hooks.json' to reflect the new output file name
  4. missing Gherkin behaviour test for hook loading (e2e/behaviour/features/dispatch/hooks-loaded.feature): Added hooks-loaded.feature that dispatches a custom harness, attempts to fetch the cloud metadata endpoint (169.254.169.254), and asserts the SSRF PreToolUse hook blocks it. This catches the 'silently not loaded' regression class where hook wiring exists but Claude Code never reads it.

Tests: passed

Next steps:

  • /fs-review — request a re-review of the changes
  • /fs-fix <your instruction> — run another fix pass with specific guidance
  • Push commits directly — review re-runs automatically on push
    Updated by fullsend fix agent

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 20, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 12:07 PM UTC · Completed 12:25 PM UTC

Commit: 45ccde0 · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added component/sandbox OpenShell sandbox environment component/runner Agent runner behavior and lifecycle labels Aug 20, 2026
@maruiz93

Copy link
Copy Markdown
Contributor

/fs-fix Rebase onto main and fix any coverage gaps.

@fullsend-ai-coder

fullsend-ai-coder Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Finished Fix · ✅ Success · Started 1:48 PM UTC · Completed 1:59 PM UTC

Commit: 45ccde0 · View workflow run →

installClaudeHooks wrote hook wiring to
/sandbox/workspace/.claude/settings.json, but buildRunCommand starts
Claude Code from /sandbox/workspace/<repo>. Claude Code only reads
project settings from <cwd>/.claude/settings.json, so the security
hooks were silently never loaded.

Move hook scripts and settings to /sandbox/claude-config/ (the
runner-owned config directory, outside the agent-writable workspace
tree) and pass the settings file via --settings in buildRunCommand.
--settings takes precedence over project/local settings, ensuring
the runner's security hooks are always active.

Changes:
- security.SandboxHooksDir → /sandbox/claude-config/hooks
- Add security.SandboxHooksSettings → /sandbox/claude-config/hooks.json
- installClaudeHooks writes to the new paths
- buildRunCommand appends --settings when HooksSettingsPath is set
- run.go populates HooksSettingsPath when security is enabled
- Golden tests for --settings presence/absence/quoting
- docs/runtimes.md workspace layout updated

Note: pre-commit could not run (sandbox network policy blocks git
fetch for hook environment setup). go vet and go test passed. The
post-script runs authoritative pre-commit on the runner.

Closes #6358
- run.go:1773: update comment to reference claude-config/hooks/ instead
  of the old workspace/.claude/ path
- hooks.go:46: update claudeSettings comment to reference hooks.json
  instead of .claude/settings.json
- hooks.go:61: update GenerateClaudeSettings docstring to reference
  hooks.json instead of .claude/settings.json
- Add hooks-loaded.feature: Gherkin behaviour test that dispatches a
  custom harness, attempts to fetch a cloud metadata endpoint
  (169.254.169.254), and asserts the SSRF PreToolUse hook blocks it.
  Catches the "silently not loaded" regression class.

Addresses review feedback on #6405
The installClaudeHooks function had 0% test coverage. Two new tests
exercise the happy path (stub openshell) and the error path (missing
openshell), bringing the function to ~60% coverage and ensuring the
changed hook-path and settings-path lines are covered.

Addresses review feedback on #6405
@fullsend-ai-coder

Copy link
Copy Markdown
Contributor Author

🔧 Fix agent — iteration 2 (human-triggered)

Rebased branch onto origin/main (clean, no conflicts). Added 2 tests for installClaudeHooks to close the patch coverage gap — function went from 0% to 59.6% coverage. All changed lines in the diff (hooksDir, mkdirCmd format, remotePath format, Upload target) are now exercised. Pre-existing CLI test failures (TestStartFetchService, TestPostScriptEnv) confirmed present on main, not caused by this PR. internal/cli/run.go is in Codecov's ignore list.

Fixed (2):

  1. rebase onto main: Rebased branch onto origin/main. The rebase applied cleanly with no conflicts (2 commits from main: renovate/experiments-digest merge).
  2. fix coverage gaps (internal/runtime/claude_test.go): Added TestInstallClaudeHooks_HappyPath and TestInstallClaudeHooks_OpenshellNotInPath to cover the installClaudeHooks function, which had 0% coverage. The happy path test uses a stub openshell binary (matching existing Bootstrap test patterns) and exercises all changed lines (hooksDir assignment, mkdir format, remotePath format, Upload call with SandboxHooksSettings). Coverage rose from 0% to 59.6% for this function.

Tests: passed

Decision points
  • Used stub openshell pattern for installClaudeHooks test rather than refactoring to use interfaces (alternatives: Refactor sandbox.Exec/Upload to interface for testability, Test only via Bootstrap integration; rationale: Matches the existing test pattern used by TestClaudeRuntime_Bootstrap_AgentNameDest and other tests in this file. Stub openshell is the established convention for testing sandbox-dependent code.)

Next steps:

  • /fs-review — request a re-review of the changes
  • /fs-fix <your instruction> — run another fix pass with specific guidance
  • Push commits directly — review re-runs automatically on push
    Updated by fullsend fix agent

@fullsend-ai-coder
fullsend-ai-coder Bot force-pushed the agent/6358-hooks-settings-path branch from 45ccde0 to 0a16c18 Compare August 20, 2026 13:59
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 20, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 2:01 PM UTC · Completed 2:19 PM UTC

Commit: 0a16c18 · View workflow run →

@fullsend-ai-review fullsend-ai-review 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.

See the review comment for full details.

Comment thread internal/runtime/claude.go
Comment thread internal/cli/run.go

// claudeSettings represents the .claude/settings.json structure.
// claudeSettings represents the hooks.json structure for Claude Code hook wiring.
type claudeSettings struct {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] naming-coherence

The struct type claudeSettings and generator function GenerateClaudeSettings reference 'settings' while the output file is now hooks.json. The naming still works (it generates a Claude Code settings structure for --settings) but the mismatch may cause confusion.

@maruiz93

Copy link
Copy Markdown
Contributor

/fs-fix Rebase onto main, fix the remaining codecov/patch coverage gap (2 uncovered lines in internal/runtime/claude.go), and rename claudeSettings struct and GenerateClaudeSettings function in internal/security/hooks.go to reflect that the output is now hooks.json (e.g. hooksConfig/GenerateHooksConfig).

@fullsend-ai-coder

fullsend-ai-coder Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Finished Fix · ❌ Failure · Started 3:41 PM UTC · Completed 3:52 PM UTC

Commit: 0a16c18 · View workflow run →

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor Author

⚠️ Post-fix script failed — Signed-off-by rejected (exit code 1)

The fix agent completed, but the post-fix script failed before finishing.

Workflow run: https://github.com/fullsend-ai/fullsend/actions/runs/32387454311

Details:
Agent commit contains a Signed-off-by trailer. Agents must not use 'git commit -s' or append Signed-off-by trailers.
Please check the workflow logs for full details and retry with /fs-fix if appropriate.

@maruiz93

Copy link
Copy Markdown
Contributor

/fs-fix Rebase onto main, fix the remaining codecov/patch coverage gap (2 uncovered lines in internal/runtime/claude.go), and rename claudeSettings struct and GenerateClaudeSettings function in internal/security/hooks.go to reflect that the output is now hooks.json (e.g. hooksConfig/GenerateHooksConfig). Make sure all commits include a DCO sign-off (git commit -s).

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

Labels

component/runner Agent runner behavior and lifecycle component/sandbox OpenShell sandbox environment ready-for-review Agent PR ready for human review requires-manual-review Review requires human judgment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sandbox tool hooks are never loaded: settings.json is written to /sandbox/workspace/.claude but Claude Code runs from /sandbox/workspace/<repo>

1 participant