mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 07:29:42 +02:00
fix(security): deliver the hook secret to hooks + isolate its rate-limit bucket
Review fixes for COD-54: - Generated hook curl commands now present X-Codeman-Hook-Secret, read from the secret file AT EXECUTION TIME via $CODEMAN_HOOK_SECRET_FILE (exported into every managed session's env by tmux buildEnvExports / the direct-PTY env builders). Without this, every local hook 401'd the moment a managed tunnel came up — the enforcement existed but nothing presented the secret. Path-not-value keeps the secret off command lines and out of config files, and running sessions pick up a newly generated secret with no respawn; server.start() ensures the file exists up front. - Hook-secret failures now count into a DEDICATED per-IP bucket (hookSecretFailures) instead of the shared authFailures map. Legacy (pre-secret) hook configs fire constantly from 127.0.0.1; counting their 401s against the shared bucket would 429 every cookie-less loopback request — locking out the Basic-Auth login path (and, through a tunnel, every client, since tunneled traffic also arrives as 127.0.0.1). - docs/security-architecture.md: secret-gated hook exemption, dedicated bucket, COD-55 refusal, and the residual caveat for EXTERNAL loopback proxies (user-run cloudflared / tailscale serve), which the managed-tunnel probe cannot see. - test/cod54-hook-event-auth.test.ts: +3 tests — login path unaffected after hook-bucket exhaustion; generated hooks reference the header + $CODEMAN_HOOK_SECRET_FILE without embedding the value; env builders export the path only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+10
-2
@@ -3,8 +3,9 @@
|
||||
*
|
||||
* Generates `.claude/settings.local.json` with hook definitions that POST
|
||||
* to Codeman's `/api/hook-event` endpoint when Claude Code fires hooks.
|
||||
* Uses `$CODEMAN_API_URL` and `$CODEMAN_SESSION_ID` env vars (set on every
|
||||
* managed session) so the config is static per case directory.
|
||||
* Uses `$CODEMAN_API_URL`, `$CODEMAN_SESSION_ID`, and `$CODEMAN_HOOK_SECRET_FILE`
|
||||
* env vars (set on every managed session) so the config is static per case
|
||||
* directory and free of secret values.
|
||||
*
|
||||
* Key exports:
|
||||
* - `generateHooksConfig()` — returns hooks object for settings.local.json
|
||||
@@ -41,11 +42,18 @@ import { HOOK_TIMEOUT_MS } from './config/auth-config.js';
|
||||
export function generateHooksConfig(): { hooks: Record<string, unknown[]> } {
|
||||
// Read Claude Code's stdin JSON and forward it as the data field.
|
||||
// Falls back to empty object if stdin is unavailable or malformed.
|
||||
// COD-54: present the per-instance hook secret so the bypass keeps working while
|
||||
// a tunnel is running. The value is read from the secret file AT EXECUTION TIME
|
||||
// (path via $CODEMAN_HOOK_SECRET_FILE, set in every managed session's env), so it
|
||||
// never lands in this config and rotation needs no respawn. If the var/file is
|
||||
// missing the header is empty — the middleware then allows the request only on
|
||||
// the plain loopback bypass (tunnel down), same as pre-secret behavior.
|
||||
const curlCmd = (event: HookEventType) =>
|
||||
`HOOK_DATA=$(cat 2>/dev/null || echo '{}'); ` +
|
||||
`printf '{"event":"${event}","sessionId":"%s","data":%s}' "$CODEMAN_SESSION_ID" "$HOOK_DATA" | ` +
|
||||
`curl -s -X POST "$CODEMAN_API_URL/api/hook-event" ` +
|
||||
`-H 'Content-Type: application/json' ` +
|
||||
`-H "X-Codeman-Hook-Secret: $(cat "$CODEMAN_HOOK_SECRET_FILE" 2>/dev/null)" ` +
|
||||
`--data @- ` +
|
||||
`2>/dev/null || true`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user