Feature/claude enterprise bootstrap#75
Conversation
Greptile SummaryThis 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.
Confidence Score: 2/5The 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
Important Files Changed
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
|
| # Requires: npm install -g @pegasi-ai/reins | ||
| set -e | ||
|
|
||
| REINS_ROOT="$(npm root -g 2>/dev/null)/@pegasi/reins" |
There was a problem hiding this comment.
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.
| 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 } : {}; | ||
| } |
There was a problem hiding this comment.
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.
| const matches = | ||
| toolName.startsWith(rule.tool_pattern) || | ||
| new RegExp(rule.tool_pattern, 'i').test(toolName); |
There was a problem hiding this comment.
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.
| 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); |
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.