Skip to content

Feature/claude enterprise bootstrap#75

Open
zmaj1231 wants to merge 2 commits into
pegasi-ai:mainfrom
zmaj1231:feature/claude-enterprise-bootstrap
Open

Feature/claude enterprise bootstrap#75
zmaj1231 wants to merge 2 commits into
pegasi-ai:mainfrom
zmaj1231:feature/claude-enterprise-bootstrap

Conversation

@zmaj1231

@zmaj1231 zmaj1231 commented May 22, 2026

Copy link
Copy Markdown
Contributor

Dependent on PR #73.

These changes make Reins usable as a Claude Enterprise plugin. Onboarding works using a bootstrap token, which is created by Reins Cloud and used by the admin while updating the Claude-managed settings. This token is passed into each team member's Reins install, and it is used to authorize enrollment into their Reins Cloud organization. Hooks are explicitly configured so that Claude can run a hook wrapper pre- and post-tool call, which get resolved to the real hook implementations in Reins.

@greptile-apps

greptile-apps Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces Claude Enterprise plugin support for Reins, enabling admin-managed onboarding via a bootstrap token that is redeemed for an org-scoped API key, and switches the runtime policy engine to a cloud-authoritative mode when a policy cache is present.

  • Bootstrap enrollment (src/cli/init.ts, src/storage/WatchtowerConfig.ts, src/lib/watchtower-client.ts): adds resolveWatchtowerBootstrap to read a token from env vars or the saved config, and redeemReinsBootstrap to exchange it for an API key; the init wizard now calls connectWithEnterpriseBootstrap unconditionally before the interactive flow.
  • Cloud-authoritative policy (src/plugin/cloud-policy.ts, src/plugin/cloud-policy-evaluator.ts, src/plugin/tool-interceptor.ts): when a policies.json cache exists, cloud shell and MCP rules take precedence; shell and matched-MCP tool calls return early after the cloud check, before reaching the local irreversibility scorer, destructive classifier, and memory risk forecaster.
  • Hook consolidation (plugin/hooks/hooks.json, plugin/bin/*): per-tool hook entries are replaced with a single wildcard entry using ${CLAUDE_PLUGIN_ROOT}, and the shebang is updated to #!/usr/bin/env sh.

Confidence Score: 2/5

The cloud-authoritative enforcement path silently disables all local safety protections for shell commands, and the hook scripts reference the wrong package installation path, which could leave enforcement entirely inactive.

Three issues affect the core security path. The cloud shell early-return in tool-interceptor.ts means that once any policies.json cache exists, the irreversibility scorer, destructive action classifier, memory risk forecaster, and trust rate limiter are all bypassed for every shell call — a catastrophic command with no matching cloud BLOCK rule goes through unchecked. The hook scripts still resolve REINS_ROOT via @pegasi/reins while the comments say @pegasi-ai/reins, so on a renamed-package install every hook silently exits 0. The bootstrap token is also re-redeemed unconditionally on every reins init run because saveWatchtowerSettings preserves the token via its merge semantics, causing repeated redemption failures or key rotation for any returning user.

src/plugin/tool-interceptor.ts (early-return cloud bypass), plugin/bin/reins-pre-hook and plugin/bin/reins-post-hook (stale package path), and src/cli/init.ts (unconditional re-enrollment logic).

Important Files Changed

Filename Overview
src/plugin/tool-interceptor.ts Adds cloud shell and MCP policy enforcement, but the early-return paths bypass all local safety checks (irreversibility, destructive gating, memory risk, trust rate limiter) when cloud policy is active.
src/cli/init.ts Adds enterprise bootstrap path; bootstrap token is unconditionally re-redeemed on every init run if a token is found, without checking for already-valid credentials.
plugin/bin/reins-pre-hook Comment updated to reference @pegasi-ai/reins but REINS_ROOT still resolves via the old @pegasi/reins path, which would silently disable hook enforcement if the package was renamed.
plugin/bin/reins-post-hook Same package name inconsistency as reins-pre-hook: comment says @pegasi-ai/reins but REINS_ROOT uses @pegasi/reins.
src/plugin/cloud-policy-evaluator.ts New evaluator for cloud shell and MCP rules; startsWith prefix check before regex may cause unintended broad matches on MCP tool names.
src/plugin/cloud-policy.ts New cloud policy cache loader and runtime policy resolver; correctly falls back to local policy when no cloud cache is present.
src/storage/WatchtowerConfig.ts Adds bootstrapToken to WatchtowerSettings and resolveWatchtowerBootstrap; merge semantics in saveWatchtowerSettings mean bootstrapToken persists after enrollment.
src/lib/watchtower-client.ts Adds redeemReinsBootstrap function and updates fetchShellPolicies endpoint; response parsing is careful and well-guarded.
src/plugin/index.ts Switches to cloud-authoritative runtime policy when cloud cache exists; local policy is still used as fallback when no cloud cache is present.
plugin/hooks/hooks.json Consolidates per-tool hook entries into single wildcard entries using ${CLAUDE_PLUGIN_ROOT}; cleaner and correct.
test/bootstrap-client.test.mjs New tests covering bootstrap token resolution, HTTP mock for token redemption, and config persistence; well-structured with proper env var cleanup.
test/policy.test.mjs Adds cloud policy tests verifying cloud-authoritative override behaviour; uses temp directories properly and cleans up after each test.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[reins init] --> B[connectWithEnterpriseBootstrap]
    B --> C{bootstrapToken present?}
    C -- No --> D[Interactive signup flow]
    C -- Yes --> E[redeemReinsBootstrap POST /api/auth/reins-bootstrap/redeem]
    E --> F[saveWatchtowerSettings merges apiKey into config\nbootstrapToken remains in config]
    F --> G[installClaudeCodeHooks]
    G --> H[Done]
    D --> H

    subgraph PreToolUse Hook
    I[createToolCallHook called] --> J[loadCloudPolicyCache]
    J --> K{cloudPolicy present?}
    K -- Yes, Shell tool --> L[evaluateCloudShellRule]
    L --> M{BLOCK?}
    M -- Yes --> N[return block:true]
    M -- No --> O[return early BYPASSES local checks]
    K -- Yes, MCP match --> P[evaluateCloudMcpRule]
    P --> Q{BLOCK?}
    Q -- Yes --> N
    Q -- No, matched --> R[return early BYPASSES local checks]
    K -- No cloud, or unmatched --> S[scoreIrreversibility / classifyDestructiveAction / memoryForecaster / trustRateLimiter / interceptor.evaluate]
    end
Loading

Comments Outside Diff (1)

  1. src/cli/init.ts, line 120-160 (link)

    P1 Bootstrap token re-redeemed on every reins init run

    connectWithEnterpriseBootstrap calls redeemReinsBootstrap whenever a token is present, without first checking whether valid API credentials already exist. After a successful first enrollment, saveWatchtowerSettings merges (not replaces) config.watchtower, so the bootstrapToken field persists in config.json. Every subsequent reins init will therefore find the token via saved?.bootstrapToken and attempt another redemption. If the token is single-use (typical for bootstrap tokens), subsequent calls fail and leave the user in a warning state; if it's multi-use, a fresh API key is provisioned on every run, invalidating the previous one.

Reviews (1): Last reviewed commit: "Implementation of Claude bootstrap" | Re-trigger Greptile

Comment thread plugin/bin/reins-pre-hook
# Requires: npm install -g @pegasi-ai/reins
set -e

REINS_ROOT="$(npm root -g 2>/dev/null)/@pegasi/reins"

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.

P1 Stale package path in REINS_ROOT

The comment was updated to @pegasi-ai/reins but REINS_ROOT still resolves via @pegasi/reins. If the globally-installed package has been (or will be) published under the new @pegasi-ai/reins scope, the [ ! -f "$HOOK" ] guard will be true and the pre-hook will emit only a WARN and exit 0, silently skipping all enforcement. The same mismatch exists in reins-post-hook.

Comment on lines +130 to +180
if (
cloudPolicy &&
moduleName === 'Shell' &&
['bash', 'exec'].includes(methodName)
) {
const command = typeof params.command === 'string' ? params.command : '';
const matchedRule = evaluateCloudShellRule(command, cloudPolicy);

if (matchedRule?.action === 'BLOCK') {
const reason =
`Reins Cloud policy blocked shell command. ` +
`${matchedRule.description || 'No description provided.'} ` +
`[rule: ${matchedRule.rule}; severity: ${matchedRule.severity || 'unknown'}]`;
await DecisionLog.append({
timestamp: new Date().toISOString(),
module: moduleName,
method: methodName,
args: [params],
decision: 'BLOCKED',
decisionTime: 0,
reason,
eventType: 'tool_blocked',
tool: toolName,
});
return { block: true, blockReason: reason };
}

if (matchedRule?.action === 'WARN') {
logger.warn('Reins Cloud shell rule matched with WARN action', {
toolName,
command,
rule: matchedRule.rule,
severity: matchedRule.severity,
});
}

await DecisionLog.append({
timestamp: new Date().toISOString(),
module: moduleName,
method: methodName,
args: [params],
decision: 'ALLOWED',
decisionTime: 0,
reason: matchedRule
? 'allowed by Reins Cloud shell policy cache'
: 'allowed by Reins Cloud shell policy cache (no matching block rule)',
eventType: 'tool_executed',
tool: toolName,
});
return shouldReturnParams ? { params } : {};
}

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.

P1 Cloud shell policy early return disables all downstream local safety checks

When cloudPolicy is present and a shell tool is invoked, the function returns at line 179 for any command not matched by a BLOCK rule. This early return means scoreIrreversibility, classifyDestructiveAction, memoryForecaster.assess, trustRateLimiter.getState, and interceptor.evaluate are never reached. A command like rm -rf / with no matching cloud BLOCK rule will execute with zero local friction — the irreversibility scorer, destructive gating, and the out-of-band approval flow are all bypassed. The same bypass applies to MCP tools via the matchedRule return on line 225. WARN/LOG cloud actions in particular provide no substitute for these protections.

Comment on lines +33 to +35
const matches =
toolName.startsWith(rule.tool_pattern) ||
new RegExp(rule.tool_pattern, 'i').test(toolName);

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.

P2 startsWith prefix match can produce unexpectedly broad MCP rule hits

toolName.startsWith(rule.tool_pattern) fires before the regex is evaluated. A pattern like "file" intended to match the file tool exactly would also match file_upload, file_delete, etc., triggering a BLOCK or WARN for unintended tools. Using an anchored regex as the only check (consistent with how evaluateCloudShellRule works) avoids this ambiguity.

Suggested change
const matches =
toolName.startsWith(rule.tool_pattern) ||
new RegExp(rule.tool_pattern, 'i').test(toolName);
const matches =
new RegExp(rule.tool_pattern, 'i').test(toolName);

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.

1 participant