failproofai 0.0.6-beta.2 → 0.0.6-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +7 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +2 -2
- package/.next/standalone/.next/server/app/_not-found.rsc +17 -17
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +17 -17
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +16 -16
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +16 -16
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g72weg._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09icjsf._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0okos0k._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0i5kvry._.js → [root-of-the-server]__0om-5pe._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__05akje6._.js → [root-of-the-server]__111.vxi._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_0mebn66._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +2 -2
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
- package/.next/standalone/.next/static/chunks/{05j1px0r8yzh6.js → 02dqjyv6_9mhq.js} +2 -2
- package/.next/standalone/.next/static/chunks/{14cl9poem30dq.js → 070orfsl6.xal.js} +1 -1
- package/.next/standalone/.next/static/chunks/0mir9jdxn35~s.css +1 -0
- package/.next/standalone/.next/static/chunks/{00j0rr7rh8ef8.js → 0o547jv-k_k35.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0ijk_kek9_wyx.js → 0pk2h2.mjxy.m.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0xpl.oscrakvx.js → 0rcwkbh24w38b.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1052sguyd-.ka.js → 140xx_tfr~lm_.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0npb~873.wvg3.js → 169_e4dq~1~b6.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0badv41uxa56..js → 17ne4p.1sw1jy.js} +1 -1
- package/.next/standalone/next.config.ts +6 -0
- package/.next/standalone/package.json +2 -2
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +91 -4
- package/dist/cli.mjs +1155 -54
- package/package.json +2 -2
- package/scripts/prune-standalone.mjs +128 -0
- package/src/auth/login.ts +104 -0
- package/src/auth/logout.ts +50 -0
- package/src/auth/token-store.ts +64 -0
- package/src/hooks/builtin-policies.ts +22 -20
- package/src/hooks/handler.ts +35 -15
- package/src/relay/daemon.ts +362 -0
- package/src/relay/pid.ts +76 -0
- package/src/relay/queue.ts +225 -0
- package/.next/standalone/.claude/settings.json +0 -316
- package/.next/standalone/.failproofai/policies/review-policies.mjs +0 -113
- package/.next/standalone/.failproofai/policies/workflow-policies.mjs +0 -63
- package/.next/standalone/.failproofai/policies-config.json +0 -39
- package/.next/standalone/.next/static/chunks/0gu_a.a80ritd.css +0 -1
- package/.next/standalone/AGENTS.md +0 -80
- package/.next/standalone/CHANGELOG.md +0 -151
- package/.next/standalone/CLAUDE.md +0 -165
- package/.next/standalone/CONTRIBUTING.md +0 -76
- package/.next/standalone/Dockerfile.docs +0 -12
- package/.next/standalone/LICENSE +0 -42
- package/.next/standalone/README.md +0 -301
- package/.next/standalone/bin/failproofai.mjs +0 -352
- package/.next/standalone/bun.lock +0 -1119
- package/.next/standalone/components.json +0 -23
- package/.next/standalone/dist/cli.mjs +0 -3595
- package/.next/standalone/dist/index.js +0 -80
- package/.next/standalone/docs/ar/architecture.mdx +0 -334
- package/.next/standalone/docs/ar/built-in-policies.mdx +0 -574
- package/.next/standalone/docs/ar/cli/dashboard.mdx +0 -28
- package/.next/standalone/docs/ar/cli/environment-variables.mdx +0 -34
- package/.next/standalone/docs/ar/cli/hook.mdx +0 -31
- package/.next/standalone/docs/ar/cli/install-policies.mdx +0 -48
- package/.next/standalone/docs/ar/cli/list-policies.mdx +0 -31
- package/.next/standalone/docs/ar/cli/remove-policies.mdx +0 -43
- package/.next/standalone/docs/ar/cli/version.mdx +0 -13
- package/.next/standalone/docs/ar/configuration.mdx +0 -223
- package/.next/standalone/docs/ar/custom-policies.mdx +0 -354
- package/.next/standalone/docs/ar/dashboard.mdx +0 -142
- package/.next/standalone/docs/ar/examples.mdx +0 -307
- package/.next/standalone/docs/ar/for-agents.mdx +0 -39
- package/.next/standalone/docs/ar/getting-started.mdx +0 -187
- package/.next/standalone/docs/ar/introduction.mdx +0 -58
- package/.next/standalone/docs/ar/package-aliases.mdx +0 -82
- package/.next/standalone/docs/ar/testing.mdx +0 -261
- package/.next/standalone/docs/architecture.mdx +0 -332
- package/.next/standalone/docs/built-in-policies.mdx +0 -574
- package/.next/standalone/docs/cli/dashboard.mdx +0 -28
- package/.next/standalone/docs/cli/environment-variables.mdx +0 -34
- package/.next/standalone/docs/cli/hook.mdx +0 -30
- package/.next/standalone/docs/cli/install-policies.mdx +0 -47
- package/.next/standalone/docs/cli/list-policies.mdx +0 -31
- package/.next/standalone/docs/cli/remove-policies.mdx +0 -43
- package/.next/standalone/docs/cli/version.mdx +0 -12
- package/.next/standalone/docs/configuration.mdx +0 -222
- package/.next/standalone/docs/custom-policies.mdx +0 -353
- package/.next/standalone/docs/dashboard.mdx +0 -142
- package/.next/standalone/docs/de/architecture.mdx +0 -332
- package/.next/standalone/docs/de/built-in-policies.mdx +0 -574
- package/.next/standalone/docs/de/cli/dashboard.mdx +0 -28
- package/.next/standalone/docs/de/cli/environment-variables.mdx +0 -34
- package/.next/standalone/docs/de/cli/hook.mdx +0 -30
- package/.next/standalone/docs/de/cli/install-policies.mdx +0 -47
- package/.next/standalone/docs/de/cli/list-policies.mdx +0 -31
- package/.next/standalone/docs/de/cli/remove-policies.mdx +0 -43
- package/.next/standalone/docs/de/cli/version.mdx +0 -12
- package/.next/standalone/docs/de/configuration.mdx +0 -222
- package/.next/standalone/docs/de/custom-policies.mdx +0 -353
- package/.next/standalone/docs/de/dashboard.mdx +0 -142
- package/.next/standalone/docs/de/examples.mdx +0 -307
- package/.next/standalone/docs/de/for-agents.mdx +0 -38
- package/.next/standalone/docs/de/getting-started.mdx +0 -186
- package/.next/standalone/docs/de/introduction.mdx +0 -57
- package/.next/standalone/docs/de/package-aliases.mdx +0 -82
- package/.next/standalone/docs/de/testing.mdx +0 -260
- package/.next/standalone/docs/docs.json +0 -1002
- package/.next/standalone/docs/es/architecture.mdx +0 -332
- package/.next/standalone/docs/es/built-in-policies.mdx +0 -574
- package/.next/standalone/docs/es/cli/dashboard.mdx +0 -28
- package/.next/standalone/docs/es/cli/environment-variables.mdx +0 -34
- package/.next/standalone/docs/es/cli/hook.mdx +0 -30
- package/.next/standalone/docs/es/cli/install-policies.mdx +0 -47
- package/.next/standalone/docs/es/cli/list-policies.mdx +0 -31
- package/.next/standalone/docs/es/cli/remove-policies.mdx +0 -43
- package/.next/standalone/docs/es/cli/version.mdx +0 -12
- package/.next/standalone/docs/es/configuration.mdx +0 -222
- package/.next/standalone/docs/es/custom-policies.mdx +0 -353
- package/.next/standalone/docs/es/dashboard.mdx +0 -142
- package/.next/standalone/docs/es/examples.mdx +0 -307
- package/.next/standalone/docs/es/for-agents.mdx +0 -38
- package/.next/standalone/docs/es/getting-started.mdx +0 -186
- package/.next/standalone/docs/es/introduction.mdx +0 -57
- package/.next/standalone/docs/es/package-aliases.mdx +0 -82
- package/.next/standalone/docs/es/testing.mdx +0 -260
- package/.next/standalone/docs/examples.mdx +0 -307
- package/.next/standalone/docs/favicon.ico +0 -0
- package/.next/standalone/docs/for-agents.mdx +0 -38
- package/.next/standalone/docs/fr/architecture.mdx +0 -332
- package/.next/standalone/docs/fr/built-in-policies.mdx +0 -574
- package/.next/standalone/docs/fr/cli/dashboard.mdx +0 -28
- package/.next/standalone/docs/fr/cli/environment-variables.mdx +0 -34
- package/.next/standalone/docs/fr/cli/hook.mdx +0 -30
- package/.next/standalone/docs/fr/cli/install-policies.mdx +0 -47
- package/.next/standalone/docs/fr/cli/list-policies.mdx +0 -31
- package/.next/standalone/docs/fr/cli/remove-policies.mdx +0 -43
- package/.next/standalone/docs/fr/cli/version.mdx +0 -12
- package/.next/standalone/docs/fr/configuration.mdx +0 -222
- package/.next/standalone/docs/fr/custom-policies.mdx +0 -353
- package/.next/standalone/docs/fr/dashboard.mdx +0 -142
- package/.next/standalone/docs/fr/examples.mdx +0 -307
- package/.next/standalone/docs/fr/for-agents.mdx +0 -38
- package/.next/standalone/docs/fr/getting-started.mdx +0 -186
- package/.next/standalone/docs/fr/introduction.mdx +0 -57
- package/.next/standalone/docs/fr/package-aliases.mdx +0 -82
- package/.next/standalone/docs/fr/testing.mdx +0 -260
- package/.next/standalone/docs/getting-started.mdx +0 -186
- package/.next/standalone/docs/he/architecture.mdx +0 -333
- package/.next/standalone/docs/he/built-in-policies.mdx +0 -574
- package/.next/standalone/docs/he/cli/dashboard.mdx +0 -28
- package/.next/standalone/docs/he/cli/environment-variables.mdx +0 -34
- package/.next/standalone/docs/he/cli/hook.mdx +0 -30
- package/.next/standalone/docs/he/cli/install-policies.mdx +0 -47
- package/.next/standalone/docs/he/cli/list-policies.mdx +0 -32
- package/.next/standalone/docs/he/cli/remove-policies.mdx +0 -43
- package/.next/standalone/docs/he/cli/version.mdx +0 -12
- package/.next/standalone/docs/he/configuration.mdx +0 -223
- package/.next/standalone/docs/he/custom-policies.mdx +0 -353
- package/.next/standalone/docs/he/dashboard.mdx +0 -142
- package/.next/standalone/docs/he/examples.mdx +0 -307
- package/.next/standalone/docs/he/for-agents.mdx +0 -38
- package/.next/standalone/docs/he/getting-started.mdx +0 -186
- package/.next/standalone/docs/he/introduction.mdx +0 -57
- package/.next/standalone/docs/he/package-aliases.mdx +0 -82
- package/.next/standalone/docs/he/testing.mdx +0 -260
- package/.next/standalone/docs/hi/architecture.mdx +0 -334
- package/.next/standalone/docs/hi/built-in-policies.mdx +0 -576
- package/.next/standalone/docs/hi/cli/dashboard.mdx +0 -28
- package/.next/standalone/docs/hi/cli/environment-variables.mdx +0 -34
- package/.next/standalone/docs/hi/cli/hook.mdx +0 -30
- package/.next/standalone/docs/hi/cli/install-policies.mdx +0 -47
- package/.next/standalone/docs/hi/cli/list-policies.mdx +0 -31
- package/.next/standalone/docs/hi/cli/remove-policies.mdx +0 -43
- package/.next/standalone/docs/hi/cli/version.mdx +0 -12
- package/.next/standalone/docs/hi/configuration.mdx +0 -222
- package/.next/standalone/docs/hi/custom-policies.mdx +0 -354
- package/.next/standalone/docs/hi/dashboard.mdx +0 -142
- package/.next/standalone/docs/hi/examples.mdx +0 -309
- package/.next/standalone/docs/hi/for-agents.mdx +0 -38
- package/.next/standalone/docs/hi/getting-started.mdx +0 -187
- package/.next/standalone/docs/hi/introduction.mdx +0 -57
- package/.next/standalone/docs/hi/package-aliases.mdx +0 -82
- package/.next/standalone/docs/hi/testing.mdx +0 -260
- package/.next/standalone/docs/i18n/README.ar.md +0 -312
- package/.next/standalone/docs/i18n/README.de.md +0 -307
- package/.next/standalone/docs/i18n/README.es.md +0 -307
- package/.next/standalone/docs/i18n/README.fr.md +0 -307
- package/.next/standalone/docs/i18n/README.he.md +0 -312
- package/.next/standalone/docs/i18n/README.hi.md +0 -307
- package/.next/standalone/docs/i18n/README.it.md +0 -307
- package/.next/standalone/docs/i18n/README.ja.md +0 -307
- package/.next/standalone/docs/i18n/README.ko.md +0 -307
- package/.next/standalone/docs/i18n/README.pt-br.md +0 -307
- package/.next/standalone/docs/i18n/README.ru.md +0 -308
- package/.next/standalone/docs/i18n/README.tr.md +0 -307
- package/.next/standalone/docs/i18n/README.vi.md +0 -307
- package/.next/standalone/docs/i18n/README.zh.md +0 -307
- package/.next/standalone/docs/introduction.mdx +0 -57
- package/.next/standalone/docs/it/architecture.mdx +0 -334
- package/.next/standalone/docs/it/built-in-policies.mdx +0 -574
- package/.next/standalone/docs/it/cli/dashboard.mdx +0 -28
- package/.next/standalone/docs/it/cli/environment-variables.mdx +0 -34
- package/.next/standalone/docs/it/cli/hook.mdx +0 -30
- package/.next/standalone/docs/it/cli/install-policies.mdx +0 -47
- package/.next/standalone/docs/it/cli/list-policies.mdx +0 -31
- package/.next/standalone/docs/it/cli/remove-policies.mdx +0 -43
- package/.next/standalone/docs/it/cli/version.mdx +0 -12
- package/.next/standalone/docs/it/configuration.mdx +0 -222
- package/.next/standalone/docs/it/custom-policies.mdx +0 -353
- package/.next/standalone/docs/it/dashboard.mdx +0 -142
- package/.next/standalone/docs/it/examples.mdx +0 -307
- package/.next/standalone/docs/it/for-agents.mdx +0 -38
- package/.next/standalone/docs/it/getting-started.mdx +0 -186
- package/.next/standalone/docs/it/introduction.mdx +0 -57
- package/.next/standalone/docs/it/package-aliases.mdx +0 -82
- package/.next/standalone/docs/it/testing.mdx +0 -260
- package/.next/standalone/docs/ja/architecture.mdx +0 -332
- package/.next/standalone/docs/ja/built-in-policies.mdx +0 -572
- package/.next/standalone/docs/ja/cli/dashboard.mdx +0 -28
- package/.next/standalone/docs/ja/cli/environment-variables.mdx +0 -34
- package/.next/standalone/docs/ja/cli/hook.mdx +0 -30
- package/.next/standalone/docs/ja/cli/install-policies.mdx +0 -47
- package/.next/standalone/docs/ja/cli/list-policies.mdx +0 -31
- package/.next/standalone/docs/ja/cli/remove-policies.mdx +0 -43
- package/.next/standalone/docs/ja/cli/version.mdx +0 -12
- package/.next/standalone/docs/ja/configuration.mdx +0 -222
- package/.next/standalone/docs/ja/custom-policies.mdx +0 -353
- package/.next/standalone/docs/ja/dashboard.mdx +0 -142
- package/.next/standalone/docs/ja/examples.mdx +0 -307
- package/.next/standalone/docs/ja/for-agents.mdx +0 -38
- package/.next/standalone/docs/ja/getting-started.mdx +0 -186
- package/.next/standalone/docs/ja/introduction.mdx +0 -57
- package/.next/standalone/docs/ja/package-aliases.mdx +0 -82
- package/.next/standalone/docs/ja/testing.mdx +0 -260
- package/.next/standalone/docs/ko/architecture.mdx +0 -332
- package/.next/standalone/docs/ko/built-in-policies.mdx +0 -572
- package/.next/standalone/docs/ko/cli/dashboard.mdx +0 -28
- package/.next/standalone/docs/ko/cli/environment-variables.mdx +0 -34
- package/.next/standalone/docs/ko/cli/hook.mdx +0 -30
- package/.next/standalone/docs/ko/cli/install-policies.mdx +0 -47
- package/.next/standalone/docs/ko/cli/list-policies.mdx +0 -31
- package/.next/standalone/docs/ko/cli/remove-policies.mdx +0 -43
- package/.next/standalone/docs/ko/cli/version.mdx +0 -12
- package/.next/standalone/docs/ko/configuration.mdx +0 -222
- package/.next/standalone/docs/ko/custom-policies.mdx +0 -353
- package/.next/standalone/docs/ko/dashboard.mdx +0 -142
- package/.next/standalone/docs/ko/examples.mdx +0 -307
- package/.next/standalone/docs/ko/for-agents.mdx +0 -38
- package/.next/standalone/docs/ko/getting-started.mdx +0 -186
- package/.next/standalone/docs/ko/introduction.mdx +0 -57
- package/.next/standalone/docs/ko/package-aliases.mdx +0 -82
- package/.next/standalone/docs/ko/testing.mdx +0 -260
- package/.next/standalone/docs/logo/dark.svg +0 -21
- package/.next/standalone/docs/logo/exosphere-dark.png +0 -0
- package/.next/standalone/docs/logo/exosphere-light.png +0 -0
- package/.next/standalone/docs/logo/light.svg +0 -21
- package/.next/standalone/docs/package-aliases.mdx +0 -82
- package/.next/standalone/docs/pt-br/architecture.mdx +0 -332
- package/.next/standalone/docs/pt-br/built-in-policies.mdx +0 -574
- package/.next/standalone/docs/pt-br/cli/dashboard.mdx +0 -28
- package/.next/standalone/docs/pt-br/cli/environment-variables.mdx +0 -34
- package/.next/standalone/docs/pt-br/cli/hook.mdx +0 -30
- package/.next/standalone/docs/pt-br/cli/install-policies.mdx +0 -47
- package/.next/standalone/docs/pt-br/cli/list-policies.mdx +0 -31
- package/.next/standalone/docs/pt-br/cli/remove-policies.mdx +0 -43
- package/.next/standalone/docs/pt-br/cli/version.mdx +0 -12
- package/.next/standalone/docs/pt-br/configuration.mdx +0 -222
- package/.next/standalone/docs/pt-br/custom-policies.mdx +0 -353
- package/.next/standalone/docs/pt-br/dashboard.mdx +0 -142
- package/.next/standalone/docs/pt-br/examples.mdx +0 -307
- package/.next/standalone/docs/pt-br/for-agents.mdx +0 -38
- package/.next/standalone/docs/pt-br/getting-started.mdx +0 -186
- package/.next/standalone/docs/pt-br/introduction.mdx +0 -57
- package/.next/standalone/docs/pt-br/package-aliases.mdx +0 -82
- package/.next/standalone/docs/pt-br/testing.mdx +0 -260
- package/.next/standalone/docs/ru/architecture.mdx +0 -333
- package/.next/standalone/docs/ru/built-in-policies.mdx +0 -574
- package/.next/standalone/docs/ru/cli/dashboard.mdx +0 -28
- package/.next/standalone/docs/ru/cli/environment-variables.mdx +0 -34
- package/.next/standalone/docs/ru/cli/hook.mdx +0 -30
- package/.next/standalone/docs/ru/cli/install-policies.mdx +0 -48
- package/.next/standalone/docs/ru/cli/list-policies.mdx +0 -32
- package/.next/standalone/docs/ru/cli/remove-policies.mdx +0 -43
- package/.next/standalone/docs/ru/cli/version.mdx +0 -12
- package/.next/standalone/docs/ru/configuration.mdx +0 -222
- package/.next/standalone/docs/ru/custom-policies.mdx +0 -354
- package/.next/standalone/docs/ru/dashboard.mdx +0 -142
- package/.next/standalone/docs/ru/examples.mdx +0 -309
- package/.next/standalone/docs/ru/for-agents.mdx +0 -38
- package/.next/standalone/docs/ru/getting-started.mdx +0 -186
- package/.next/standalone/docs/ru/introduction.mdx +0 -57
- package/.next/standalone/docs/ru/package-aliases.mdx +0 -82
- package/.next/standalone/docs/ru/testing.mdx +0 -260
- package/.next/standalone/docs/testing.mdx +0 -260
- package/.next/standalone/docs/tr/architecture.mdx +0 -333
- package/.next/standalone/docs/tr/built-in-policies.mdx +0 -574
- package/.next/standalone/docs/tr/cli/dashboard.mdx +0 -28
- package/.next/standalone/docs/tr/cli/environment-variables.mdx +0 -34
- package/.next/standalone/docs/tr/cli/hook.mdx +0 -30
- package/.next/standalone/docs/tr/cli/install-policies.mdx +0 -47
- package/.next/standalone/docs/tr/cli/list-policies.mdx +0 -31
- package/.next/standalone/docs/tr/cli/remove-policies.mdx +0 -44
- package/.next/standalone/docs/tr/cli/version.mdx +0 -12
- package/.next/standalone/docs/tr/configuration.mdx +0 -222
- package/.next/standalone/docs/tr/custom-policies.mdx +0 -353
- package/.next/standalone/docs/tr/dashboard.mdx +0 -142
- package/.next/standalone/docs/tr/examples.mdx +0 -308
- package/.next/standalone/docs/tr/for-agents.mdx +0 -38
- package/.next/standalone/docs/tr/getting-started.mdx +0 -186
- package/.next/standalone/docs/tr/introduction.mdx +0 -57
- package/.next/standalone/docs/tr/package-aliases.mdx +0 -82
- package/.next/standalone/docs/tr/testing.mdx +0 -260
- package/.next/standalone/docs/vi/architecture.mdx +0 -334
- package/.next/standalone/docs/vi/built-in-policies.mdx +0 -575
- package/.next/standalone/docs/vi/cli/dashboard.mdx +0 -28
- package/.next/standalone/docs/vi/cli/environment-variables.mdx +0 -34
- package/.next/standalone/docs/vi/cli/hook.mdx +0 -30
- package/.next/standalone/docs/vi/cli/install-policies.mdx +0 -47
- package/.next/standalone/docs/vi/cli/list-policies.mdx +0 -31
- package/.next/standalone/docs/vi/cli/remove-policies.mdx +0 -43
- package/.next/standalone/docs/vi/cli/version.mdx +0 -13
- package/.next/standalone/docs/vi/configuration.mdx +0 -222
- package/.next/standalone/docs/vi/custom-policies.mdx +0 -353
- package/.next/standalone/docs/vi/dashboard.mdx +0 -142
- package/.next/standalone/docs/vi/examples.mdx +0 -308
- package/.next/standalone/docs/vi/for-agents.mdx +0 -38
- package/.next/standalone/docs/vi/getting-started.mdx +0 -186
- package/.next/standalone/docs/vi/introduction.mdx +0 -57
- package/.next/standalone/docs/vi/package-aliases.mdx +0 -82
- package/.next/standalone/docs/vi/testing.mdx +0 -260
- package/.next/standalone/docs/zh/architecture.mdx +0 -332
- package/.next/standalone/docs/zh/built-in-policies.mdx +0 -572
- package/.next/standalone/docs/zh/cli/dashboard.mdx +0 -28
- package/.next/standalone/docs/zh/cli/environment-variables.mdx +0 -34
- package/.next/standalone/docs/zh/cli/hook.mdx +0 -30
- package/.next/standalone/docs/zh/cli/install-policies.mdx +0 -47
- package/.next/standalone/docs/zh/cli/list-policies.mdx +0 -31
- package/.next/standalone/docs/zh/cli/remove-policies.mdx +0 -43
- package/.next/standalone/docs/zh/cli/version.mdx +0 -12
- package/.next/standalone/docs/zh/configuration.mdx +0 -222
- package/.next/standalone/docs/zh/custom-policies.mdx +0 -353
- package/.next/standalone/docs/zh/dashboard.mdx +0 -142
- package/.next/standalone/docs/zh/examples.mdx +0 -307
- package/.next/standalone/docs/zh/for-agents.mdx +0 -38
- package/.next/standalone/docs/zh/getting-started.mdx +0 -186
- package/.next/standalone/docs/zh/introduction.mdx +0 -57
- package/.next/standalone/docs/zh/package-aliases.mdx +0 -82
- package/.next/standalone/docs/zh/testing.mdx +0 -260
- package/.next/standalone/eslint.config.mjs +0 -15
- package/.next/standalone/examples/convention-policies/security-policies.mjs +0 -40
- package/.next/standalone/examples/convention-policies/workflow-policies.mjs +0 -41
- package/.next/standalone/examples/policies-advanced/index.js +0 -103
- package/.next/standalone/examples/policies-advanced/utils.js +0 -35
- package/.next/standalone/examples/policies-basic.js +0 -77
- package/.next/standalone/examples/policies-notification.js +0 -104
- package/.next/standalone/node_modules/@img/colour/color.cjs +0 -1594
- package/.next/standalone/node_modules/@img/colour/index.cjs +0 -1
- package/.next/standalone/node_modules/@img/colour/package.json +0 -45
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/README.md +0 -46
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/glib-2.0/include/glibconfig.h +0 -221
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/index.js +0 -1
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/libvips-cpp.so.8.17.3 +0 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/package.json +0 -42
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/versions.json +0 -30
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/README.md +0 -46
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/glib-2.0/include/glibconfig.h +0 -221
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +0 -1
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 +0 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +0 -42
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +0 -30
- package/.next/standalone/node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64.node +0 -0
- package/.next/standalone/node_modules/@img/sharp-linux-x64/package.json +0 -46
- package/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node +0 -0
- package/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +0 -46
- package/.next/standalone/node_modules/detect-libc/lib/detect-libc.js +0 -313
- package/.next/standalone/node_modules/detect-libc/lib/elf.js +0 -39
- package/.next/standalone/node_modules/detect-libc/lib/filesystem.js +0 -51
- package/.next/standalone/node_modules/detect-libc/lib/process.js +0 -24
- package/.next/standalone/node_modules/detect-libc/package.json +0 -44
- package/.next/standalone/node_modules/sharp/lib/channel.js +0 -177
- package/.next/standalone/node_modules/sharp/lib/colour.js +0 -195
- package/.next/standalone/node_modules/sharp/lib/composite.js +0 -212
- package/.next/standalone/node_modules/sharp/lib/constructor.js +0 -499
- package/.next/standalone/node_modules/sharp/lib/index.js +0 -16
- package/.next/standalone/node_modules/sharp/lib/input.js +0 -809
- package/.next/standalone/node_modules/sharp/lib/is.js +0 -143
- package/.next/standalone/node_modules/sharp/lib/libvips.js +0 -207
- package/.next/standalone/node_modules/sharp/lib/operation.js +0 -1016
- package/.next/standalone/node_modules/sharp/lib/output.js +0 -1666
- package/.next/standalone/node_modules/sharp/lib/resize.js +0 -595
- package/.next/standalone/node_modules/sharp/lib/sharp.js +0 -121
- package/.next/standalone/node_modules/sharp/lib/utility.js +0 -291
- package/.next/standalone/node_modules/sharp/node_modules/semver/classes/comparator.js +0 -143
- package/.next/standalone/node_modules/sharp/node_modules/semver/classes/range.js +0 -557
- package/.next/standalone/node_modules/sharp/node_modules/semver/classes/semver.js +0 -333
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/cmp.js +0 -54
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/coerce.js +0 -62
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/compare.js +0 -7
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/eq.js +0 -5
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/gt.js +0 -5
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/gte.js +0 -5
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/lt.js +0 -5
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/lte.js +0 -5
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/neq.js +0 -5
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/parse.js +0 -18
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/satisfies.js +0 -12
- package/.next/standalone/node_modules/sharp/node_modules/semver/internal/constants.js +0 -37
- package/.next/standalone/node_modules/sharp/node_modules/semver/internal/debug.js +0 -11
- package/.next/standalone/node_modules/sharp/node_modules/semver/internal/identifiers.js +0 -29
- package/.next/standalone/node_modules/sharp/node_modules/semver/internal/lrucache.js +0 -42
- package/.next/standalone/node_modules/sharp/node_modules/semver/internal/parse-options.js +0 -17
- package/.next/standalone/node_modules/sharp/node_modules/semver/internal/re.js +0 -223
- package/.next/standalone/node_modules/sharp/node_modules/semver/package.json +0 -78
- package/.next/standalone/node_modules/sharp/package.json +0 -202
- package/.next/standalone/scripts/alias-proxy.js +0 -18
- package/.next/standalone/scripts/dev.ts +0 -3
- package/.next/standalone/scripts/install-telemetry.mjs +0 -108
- package/.next/standalone/scripts/launch.ts +0 -83
- package/.next/standalone/scripts/parse-script-args.ts +0 -87
- package/.next/standalone/scripts/postinstall.mjs +0 -121
- package/.next/standalone/scripts/preuninstall.mjs +0 -131
- package/.next/standalone/scripts/publish-aliases.mjs +0 -87
- package/.next/standalone/scripts/start.ts +0 -3
- package/.next/standalone/scripts/sync-hook-events-prompt.md +0 -60
- package/.next/standalone/scripts/translate-docs/cache.ts +0 -62
- package/.next/standalone/scripts/translate-docs/cli.ts +0 -357
- package/.next/standalone/scripts/translate-docs/config.ts +0 -248
- package/.next/standalone/scripts/translate-docs/mdx-translator.ts +0 -153
- package/.next/standalone/scripts/translate-docs/mintlify-nav.ts +0 -107
- package/.next/standalone/scripts/translate-docs/readme-translator.ts +0 -154
- package/.next/standalone/scripts/translate-docs/translator.ts +0 -68
- package/.next/standalone/scripts/translate-docs/types.ts +0 -43
- package/.next/standalone/src/cli-error.ts +0 -18
- package/.next/standalone/src/hooks/builtin-policies.ts +0 -1613
- package/.next/standalone/src/hooks/custom-hooks-loader.ts +0 -205
- package/.next/standalone/src/hooks/custom-hooks-registry.ts +0 -30
- package/.next/standalone/src/hooks/handler.ts +0 -202
- package/.next/standalone/src/hooks/hook-activity-store.ts +0 -349
- package/.next/standalone/src/hooks/hook-logger.ts +0 -133
- package/.next/standalone/src/hooks/hook-telemetry.ts +0 -43
- package/.next/standalone/src/hooks/hooks-config.ts +0 -166
- package/.next/standalone/src/hooks/install-prompt.ts +0 -357
- package/.next/standalone/src/hooks/llm-client.ts +0 -90
- package/.next/standalone/src/hooks/loader-utils.ts +0 -178
- package/.next/standalone/src/hooks/manager.ts +0 -692
- package/.next/standalone/src/hooks/policy-evaluator.ts +0 -224
- package/.next/standalone/src/hooks/policy-helpers.ts +0 -16
- package/.next/standalone/src/hooks/policy-registry.ts +0 -90
- package/.next/standalone/src/hooks/policy-types.ts +0 -77
- package/.next/standalone/src/hooks/types.ts +0 -63
- package/.next/standalone/src/index.ts +0 -19
- package/.next/standalone/src/posthog-key.ts +0 -5
- package/.next/standalone/tailwind.config.ts +0 -11
- package/.next/standalone/tsconfig.json +0 -42
- package/.next/standalone/vitest.config.e2e.mts +0 -24
- package/.next/standalone/vitest.config.mts +0 -23
- /package/.next/standalone/.next/static/{A9pNTZdoYJTVyPAYwQMx5 → wOkJXoch1UmRAmyIuKZWc}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{A9pNTZdoYJTVyPAYwQMx5 → wOkJXoch1UmRAmyIuKZWc}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{A9pNTZdoYJTVyPAYwQMx5 → wOkJXoch1UmRAmyIuKZWc}/_ssgManifest.js +0 -0
|
@@ -1,1613 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Built-in security policies for Claude Code hooks.
|
|
3
|
-
*/
|
|
4
|
-
import { resolve, join } from "node:path";
|
|
5
|
-
import { readFile, writeFile, stat, open } from "node:fs/promises";
|
|
6
|
-
import { execSync, execFileSync } from "node:child_process";
|
|
7
|
-
import { homedir } from "node:os";
|
|
8
|
-
import type { BuiltinPolicyDefinition, PolicyContext, PolicyResult, PolicyParamsSchema } from "./policy-types";
|
|
9
|
-
import { allow, deny, instruct } from "./policy-helpers";
|
|
10
|
-
import { registerPolicy } from "./policy-registry";
|
|
11
|
-
import { hookLogWarn } from "./hook-logger";
|
|
12
|
-
|
|
13
|
-
function isClaudeInternalPath(resolved: string): boolean {
|
|
14
|
-
const claudeDir = join(homedir(), ".claude");
|
|
15
|
-
return resolved === claudeDir || resolved.startsWith(claudeDir + "/");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function isClaudeSettingsFile(resolved: string): boolean {
|
|
19
|
-
return /[\\/]\.claude[\\/]settings(?:\.[^/\\]+)?\.json$/.test(resolved);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function getCommand(ctx: PolicyContext): string {
|
|
23
|
-
return (ctx.toolInput?.command as string) ?? "";
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function getFilePath(ctx: PolicyContext): string {
|
|
27
|
-
return (ctx.toolInput?.file_path as string) ?? "";
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Parse a command string into argv tokens for safe pattern matching.
|
|
32
|
-
* Splits on whitespace and strips simple single/double quotes.
|
|
33
|
-
* Does not handle all shell syntax — sufficient for prefix-match allowlists.
|
|
34
|
-
*/
|
|
35
|
-
function parseArgvTokens(cmd: string): string[] {
|
|
36
|
-
return cmd.trim().split(/\s+/).map((t) => t.replace(/^['"]|['"]$/g, ""));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Shell operators that always act as command separators when whitespace-delimited.
|
|
40
|
-
const SHELL_OPERATORS = new Set(["&&", "||", "|", ";"]);
|
|
41
|
-
|
|
42
|
-
// Shell metacharacters that are unsafe when embedded inside a token. Any command
|
|
43
|
-
// whose argv contains one of these in a token is rejected before allowlist matching.
|
|
44
|
-
// This closes the bypass where operators are glued to a word (e.g. "nginx;evil" or
|
|
45
|
-
// "nginx&&evil") and would otherwise be invisible to the standalone-operator check.
|
|
46
|
-
// Note: | is intentionally excluded here because "foo|bar" is a valid grep/sed
|
|
47
|
-
// argument value; the standalone-operator check above already handles bare "|" tokens.
|
|
48
|
-
const SHELL_METACHAR_RE = /[;&<>`$()\\]/;
|
|
49
|
-
|
|
50
|
-
// -- Pre-compiled regex constants (hoisted to avoid per-call allocation) --
|
|
51
|
-
|
|
52
|
-
// sanitizeJwt
|
|
53
|
-
const JWT_RE = /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/;
|
|
54
|
-
|
|
55
|
-
// sanitizeApiKeys
|
|
56
|
-
const API_KEY_PATTERNS: Array<[RegExp, string]> = [
|
|
57
|
-
[/sk-ant-[A-Za-z0-9\-_]{20,}/, "Anthropic API key"],
|
|
58
|
-
[/sk-proj-[A-Za-z0-9\-_]{20,}/, "OpenAI project API key"],
|
|
59
|
-
[/sk-[A-Za-z0-9]{20,}/, "OpenAI API key"],
|
|
60
|
-
[/ghp_[A-Za-z0-9]{36}/, "GitHub personal access token"],
|
|
61
|
-
[/github_pat_[A-Za-z0-9_]{82}/, "GitHub fine-grained token"],
|
|
62
|
-
[/AKIA[A-Z0-9]{16}/, "AWS access key ID"],
|
|
63
|
-
[/sk_live_[A-Za-z0-9]{24,}/, "Stripe live secret key"],
|
|
64
|
-
[/sk_test_[A-Za-z0-9]{24,}/, "Stripe test secret key"],
|
|
65
|
-
[/AIza[0-9A-Za-z\-_]{35}/, "Google API key"],
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
// sanitizeConnectionStrings
|
|
69
|
-
const CONNECTION_STRING_RE = /(?:postgresql|postgres|mysql|mongodb(?:\+srv)?|redis|amqps?|smtps?):\/\/[^@\s]+@/;
|
|
70
|
-
|
|
71
|
-
// sanitizePrivateKeyContent
|
|
72
|
-
const PRIVATE_KEY_RE = /-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----/;
|
|
73
|
-
|
|
74
|
-
// sanitizeBearerTokens
|
|
75
|
-
const BEARER_TOKEN_RE = /Authorization:\s*Bearer\s+[A-Za-z0-9\-._~+/]{20,}/i;
|
|
76
|
-
|
|
77
|
-
// warnDestructiveSql / warnSchemaAlteration
|
|
78
|
-
const SQL_TOOL_RE = /\b(?:psql|mysql|sqlite3|pgcli|clickhouse-client)\b/;
|
|
79
|
-
const DESTRUCTIVE_SQL_RE = /\b(?:DROP\s+(?:TABLE|DATABASE|SCHEMA)|TRUNCATE\b)/i;
|
|
80
|
-
const DELETE_NO_WHERE_RE = /\bDELETE\s+FROM\b/i;
|
|
81
|
-
const SQL_WHERE_RE = /\bWHERE\b/i;
|
|
82
|
-
const SCHEMA_ALTER_RE = /\bALTER\s+TABLE\b[\s\S]*\b(?:DROP\s+COLUMN|ADD\s+COLUMN|RENAME\s+(?:COLUMN|TO)|MODIFY\s+COLUMN)\b/i;
|
|
83
|
-
|
|
84
|
-
// warnPackagePublish
|
|
85
|
-
const PUBLISH_CMD_RE = /(?:npm\s+publish|bun\s+publish|pnpm\s+publish|yarn\s+npm\s+publish|twine\s+upload|poetry\s+publish|cargo\s+publish|gem\s+push)\b/;
|
|
86
|
-
|
|
87
|
-
// protectEnvVars
|
|
88
|
-
const ENV_PRINTENV_RE = /(?:^|\s|;|&&|\|\|)(?:env|printenv)(?:\s|$|;|&&|\|)/;
|
|
89
|
-
const ECHO_ENV_RE = /echo\s+.*\$\{?[A-Za-z_]/;
|
|
90
|
-
const EXPORT_RE = /(?:^|\s|;|&&|\|\|)export\s+\w+/;
|
|
91
|
-
const PS_ENV_VAR_RE = /\$env:[A-Za-z_]/i;
|
|
92
|
-
const PS_CHILDITEM_ENV_RE = /(?:Get-ChildItem|dir|gci|ls)\s+Env:/i;
|
|
93
|
-
const DOTNET_GETENV_RE = /\[Environment\]::GetEnvironment/i;
|
|
94
|
-
const CMD_ECHO_ENV_RE = /echo\s+%[A-Za-z_]/i;
|
|
95
|
-
|
|
96
|
-
// blockEnvFiles
|
|
97
|
-
const ENV_FILE_PATH_RE = /(?:^|[\\/])\.env(?:\.|$)/;
|
|
98
|
-
const ENV_CMD_RE = /\.env(?:\b|\s|$|\.)/;
|
|
99
|
-
|
|
100
|
-
// blockSudo
|
|
101
|
-
const SUDO_RE = /(?:^|;|&&|\|\|)\s*sudo\s/;
|
|
102
|
-
const PS_ELEVATION_RE = /Start-Process\s+.*-Verb\s+RunAs/i;
|
|
103
|
-
const RUNAS_RE = /(?:^|;|&&|\|\|)\s*runas\s/i;
|
|
104
|
-
|
|
105
|
-
// blockCurlPipeSh
|
|
106
|
-
const CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh|dash|ksh|csh|tcsh|fish|ash)\b/;
|
|
107
|
-
const PS_WEB_PIPE_RE = /(?:Invoke-WebRequest|iwr|Invoke-RestMethod|irm)\s+.*\|\s*(?:Invoke-Expression|iex)/i;
|
|
108
|
-
|
|
109
|
-
// blockForcePush
|
|
110
|
-
const FORCE_PUSH_RE = /(?:--force|-f\b)/;
|
|
111
|
-
|
|
112
|
-
// blockSecretsWrite
|
|
113
|
-
const SECRET_FILE_RE = /\.(?:pem|key)$/;
|
|
114
|
-
const SECRET_FILE_ID_RSA_RE = /id_rsa/;
|
|
115
|
-
const SECRET_FILE_CREDENTIALS_RE = /credentials/;
|
|
116
|
-
|
|
117
|
-
// blockWorkOnMain
|
|
118
|
-
const GIT_COMMIT_MERGE_RE = /git\s+(?:commit|merge|rebase|cherry-pick)\b/;
|
|
119
|
-
|
|
120
|
-
// blockFailproofaiCommands
|
|
121
|
-
const FAILPROOFAI_CLI_RE = /(?:^|;|&&|\|\||\|)\s*failproofai(?:\s|$)/;
|
|
122
|
-
const FAILPROOFAI_UNINSTALL_RE = /(?:npm\s+(?:uninstall|remove|un|r)\s.*failproofai|bun\s+remove\s.*failproofai|yarn\s+global\s+remove\s+failproofai|pnpm\s+(?:remove|uninstall|un)\s.*failproofai)/;
|
|
123
|
-
|
|
124
|
-
// warnGitAmend
|
|
125
|
-
const GIT_AMEND_RE = /\bgit\s+commit\b.*--amend\b/;
|
|
126
|
-
|
|
127
|
-
// warnGitStashDrop
|
|
128
|
-
const GIT_STASH_DROP_RE = /\bgit\s+stash\s+(?:drop|clear)\b/;
|
|
129
|
-
|
|
130
|
-
// warnAllFilesStaged
|
|
131
|
-
const GIT_ADD_ALL_RE = /\bgit\s+add\s+(?:-A\b|--all\b|\.(?:\s|$|;|&&|\|\|))/;
|
|
132
|
-
|
|
133
|
-
// warnGlobalPackageInstall
|
|
134
|
-
const NPM_GLOBAL_RE = /\bnpm\s+(?:install|i)\b(?=.*(?:\s-g\b|--global\b))/;
|
|
135
|
-
const YARN_GLOBAL_RE = /\byarn\s+global\s+add\b/;
|
|
136
|
-
const PNPM_GLOBAL_RE = /\bpnpm\s+(?:add|install|i)\b(?=.*(?:\s-g\b|--global\b))/;
|
|
137
|
-
const BUN_GLOBAL_RE = /\bbun\s+(?:install|add)\b(?=.*(?:\s-g\b|--global\b))/;
|
|
138
|
-
const CARGO_INSTALL_RE = /\bcargo\s+install\b/;
|
|
139
|
-
const PIP_SYSTEM_RE = /\bpip(?:3)?\s+install\b(?=.*(?:--user\b|--break-system-packages\b))/;
|
|
140
|
-
|
|
141
|
-
// preferPackageManager — maps manager name → detection patterns
|
|
142
|
-
const PKG_MANAGER_DETECTORS: Record<string, RegExp[]> = {
|
|
143
|
-
pip: [/\bpip\b/, /\bpip3\b/, /\bpython3?\s+-m\s+pip\b/],
|
|
144
|
-
npm: [/\bnpm\b/, /\bnpx\b/],
|
|
145
|
-
yarn: [/\byarn\b/],
|
|
146
|
-
pnpm: [/\bpnpm\b/, /\bpnpx\b/],
|
|
147
|
-
bun: [/\bbun\b/, /\bbunx\b/],
|
|
148
|
-
uv: [/\buv\b/],
|
|
149
|
-
poetry: [/\bpoetry\b/],
|
|
150
|
-
pipenv: [/\bpipenv\b/],
|
|
151
|
-
conda: [/\bconda\b/],
|
|
152
|
-
cargo: [/\bcargo\b/],
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
// warnBackgroundProcess
|
|
156
|
-
const NOHUP_RE = /\bnohup\s+\S/;
|
|
157
|
-
const SCREEN_DETACH_RE = /\bscreen\s+-[A-Za-z]*d[A-Za-z]*\b/;
|
|
158
|
-
const TMUX_DETACH_RE = /\btmux\s+(?:new-session|new)\b[^|&;]*-d\b/;
|
|
159
|
-
const DISOWN_RE = /\bdisown\b/;
|
|
160
|
-
const BACKGROUND_AMPERSAND_RE = /(?<![&|])\s?&\s*(?:$|#|;)/;
|
|
161
|
-
|
|
162
|
-
// Caches the current branch per cwd to avoid repeated execSync calls.
|
|
163
|
-
// Trade-off: if the user switches branches externally mid-session, the cache serves
|
|
164
|
-
// the stale value until the process restarts. This is acceptable since branch switches
|
|
165
|
-
// during an active Claude session are rare.
|
|
166
|
-
const gitBranchCache = new Map<string, string>();
|
|
167
|
-
|
|
168
|
-
function getCurrentBranch(cwd: string): string | null {
|
|
169
|
-
try {
|
|
170
|
-
let branch = gitBranchCache.get(cwd);
|
|
171
|
-
if (branch === undefined) {
|
|
172
|
-
branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
173
|
-
cwd,
|
|
174
|
-
encoding: "utf8",
|
|
175
|
-
timeout: 3000,
|
|
176
|
-
}).trim();
|
|
177
|
-
gitBranchCache.set(cwd, branch);
|
|
178
|
-
}
|
|
179
|
-
return branch || null;
|
|
180
|
-
} catch {
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function getHeadSha(cwd: string): string | null {
|
|
186
|
-
try {
|
|
187
|
-
const sha = execSync("git rev-parse HEAD", {
|
|
188
|
-
cwd,
|
|
189
|
-
encoding: "utf8",
|
|
190
|
-
timeout: 3000,
|
|
191
|
-
}).trim();
|
|
192
|
-
return sha || null;
|
|
193
|
-
} catch {
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
interface CiCheck {
|
|
199
|
-
name: string;
|
|
200
|
-
status: string;
|
|
201
|
-
conclusion: string;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/** Fetch third-party check runs (non-GitHub-Actions) for a commit via the Checks API. */
|
|
205
|
-
function getThirdPartyCheckRuns(cwd: string, sha: string): CiCheck[] {
|
|
206
|
-
try {
|
|
207
|
-
const json = execFileSync(
|
|
208
|
-
"gh",
|
|
209
|
-
[
|
|
210
|
-
"api",
|
|
211
|
-
`repos/{owner}/{repo}/commits/${sha}/check-runs`,
|
|
212
|
-
"--jq",
|
|
213
|
-
'.check_runs | map(select(.app.slug != "github-actions")) | map({name: .name, status: .status, conclusion: (.conclusion // "")})',
|
|
214
|
-
],
|
|
215
|
-
{
|
|
216
|
-
cwd,
|
|
217
|
-
encoding: "utf8",
|
|
218
|
-
timeout: 15000,
|
|
219
|
-
},
|
|
220
|
-
).trim();
|
|
221
|
-
|
|
222
|
-
if (!json || json === "[]") return [];
|
|
223
|
-
return JSON.parse(json) as CiCheck[];
|
|
224
|
-
} catch {
|
|
225
|
-
return [];
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/** Fetch commit statuses (legacy Status API) and normalize to CiCheck format. */
|
|
230
|
-
function getCommitStatuses(cwd: string, sha: string): CiCheck[] {
|
|
231
|
-
try {
|
|
232
|
-
const json = execFileSync(
|
|
233
|
-
"gh",
|
|
234
|
-
[
|
|
235
|
-
"api",
|
|
236
|
-
`repos/{owner}/{repo}/commits/${sha}/statuses`,
|
|
237
|
-
"--jq",
|
|
238
|
-
'map({name: .context, state: .state}) | unique_by(.name)',
|
|
239
|
-
],
|
|
240
|
-
{
|
|
241
|
-
cwd,
|
|
242
|
-
encoding: "utf8",
|
|
243
|
-
timeout: 15000,
|
|
244
|
-
},
|
|
245
|
-
).trim();
|
|
246
|
-
|
|
247
|
-
if (!json || json === "[]") return [];
|
|
248
|
-
const statuses = JSON.parse(json) as Array<{ name: string; state: string }>;
|
|
249
|
-
return statuses.map((s) => ({
|
|
250
|
-
name: s.name,
|
|
251
|
-
status: s.state === "pending" ? "in_progress" : "completed",
|
|
252
|
-
conclusion: s.state === "pending" ? "" : s.state === "success" ? "success" : "failure",
|
|
253
|
-
}));
|
|
254
|
-
} catch {
|
|
255
|
-
return [];
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Check if a command matches an allow pattern using token-by-token comparison.
|
|
261
|
-
* The "*" token is a wildcard. Extra command tokens beyond the pattern are allowed,
|
|
262
|
-
* UNLESS any token is a standalone shell operator (&&, ||, |, ;) OR contains an
|
|
263
|
-
* embedded shell metacharacter — both cases are rejected to prevent bypass via
|
|
264
|
-
* appended sub-commands or glued operators (e.g. "nginx;" or "nginx;evil").
|
|
265
|
-
*/
|
|
266
|
-
function matchesAllowedPattern(cmd: string, pattern: string): boolean {
|
|
267
|
-
const cmdTokens = parseArgvTokens(cmd);
|
|
268
|
-
const patTokens = parseArgvTokens(pattern);
|
|
269
|
-
if (cmdTokens.length < patTokens.length) return false;
|
|
270
|
-
// Reject commands containing standalone shell-operator tokens
|
|
271
|
-
if (cmdTokens.some((tok) => SHELL_OPERATORS.has(tok))) return false;
|
|
272
|
-
// Reject any token containing embedded shell metacharacters
|
|
273
|
-
if (cmdTokens.some((tok) => SHELL_METACHAR_RE.test(tok))) return false;
|
|
274
|
-
return patTokens.every((tok, i) => tok === "*" || tok === cmdTokens[i]);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// -- Policy implementations --
|
|
278
|
-
|
|
279
|
-
function sanitizeJwt(ctx: PolicyContext): PolicyResult {
|
|
280
|
-
// PostToolUse: scrub JWT patterns from tool output
|
|
281
|
-
const output = JSON.stringify(ctx.payload);
|
|
282
|
-
if (JWT_RE.test(output)) {
|
|
283
|
-
return {
|
|
284
|
-
decision: "deny",
|
|
285
|
-
reason: "JWT token detected in tool output",
|
|
286
|
-
message: "[REDACTED: JWT token removed by failproofai]",
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
return allow();
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function sanitizeApiKeys(ctx: PolicyContext): PolicyResult {
|
|
293
|
-
// PostToolUse: scrub common API key patterns from tool output
|
|
294
|
-
const output = JSON.stringify(ctx.payload);
|
|
295
|
-
for (const [pattern, label] of API_KEY_PATTERNS) {
|
|
296
|
-
if (pattern.test(output)) {
|
|
297
|
-
return {
|
|
298
|
-
decision: "deny",
|
|
299
|
-
reason: `${label} detected in tool output`,
|
|
300
|
-
message: `[REDACTED: ${label} removed by failproofai]`,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Check additional user-configured patterns
|
|
306
|
-
const additional = ((ctx.params?.additionalPatterns ?? []) as Array<{ regex: string; label: string }>);
|
|
307
|
-
for (const { regex, label } of additional) {
|
|
308
|
-
try {
|
|
309
|
-
if (new RegExp(regex).test(output)) {
|
|
310
|
-
return {
|
|
311
|
-
decision: "deny",
|
|
312
|
-
reason: `${label} detected in tool output`,
|
|
313
|
-
message: `[REDACTED: ${label} removed by failproofai]`,
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
} catch {
|
|
317
|
-
hookLogWarn(`additionalPatterns: invalid regex "${regex}", skipping`);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
return allow();
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function sanitizeConnectionStrings(ctx: PolicyContext): PolicyResult {
|
|
325
|
-
// PostToolUse: scrub database connection strings with embedded credentials
|
|
326
|
-
const output = JSON.stringify(ctx.payload);
|
|
327
|
-
if (CONNECTION_STRING_RE.test(output)) {
|
|
328
|
-
return {
|
|
329
|
-
decision: "deny",
|
|
330
|
-
reason: "Database connection string with credentials detected in tool output",
|
|
331
|
-
message: "[REDACTED: connection string removed by failproofai]",
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
return allow();
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function sanitizePrivateKeyContent(ctx: PolicyContext): PolicyResult {
|
|
338
|
-
// PostToolUse: scrub PEM private key blocks from tool output
|
|
339
|
-
const output = JSON.stringify(ctx.payload);
|
|
340
|
-
if (PRIVATE_KEY_RE.test(output)) {
|
|
341
|
-
return {
|
|
342
|
-
decision: "deny",
|
|
343
|
-
reason: "Private key content detected in tool output",
|
|
344
|
-
message: "[REDACTED: private key content removed by failproofai]",
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
return allow();
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function sanitizeBearerTokens(ctx: PolicyContext): PolicyResult {
|
|
351
|
-
// PostToolUse: scrub Authorization: Bearer tokens from tool output
|
|
352
|
-
const output = JSON.stringify(ctx.payload);
|
|
353
|
-
if (BEARER_TOKEN_RE.test(output)) {
|
|
354
|
-
return {
|
|
355
|
-
decision: "deny",
|
|
356
|
-
reason: "Bearer token detected in tool output",
|
|
357
|
-
message: "[REDACTED: Bearer token removed by failproofai]",
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
return allow();
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function warnDestructiveSql(ctx: PolicyContext): PolicyResult {
|
|
364
|
-
if (ctx.toolName !== "Bash") return allow();
|
|
365
|
-
const cmd = getCommand(ctx);
|
|
366
|
-
if (!SQL_TOOL_RE.test(cmd)) return allow();
|
|
367
|
-
|
|
368
|
-
// DROP or TRUNCATE always warns
|
|
369
|
-
if (DESTRUCTIVE_SQL_RE.test(cmd)) {
|
|
370
|
-
return instruct(
|
|
371
|
-
"STOP: This command contains destructive SQL (DROP/TRUNCATE/DELETE). Confirm with the user before executing.",
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// DELETE FROM without WHERE warns
|
|
376
|
-
if (DELETE_NO_WHERE_RE.test(cmd) && !SQL_WHERE_RE.test(cmd)) {
|
|
377
|
-
return instruct(
|
|
378
|
-
"STOP: This command contains destructive SQL (DROP/TRUNCATE/DELETE). Confirm with the user before executing.",
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
return allow();
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function warnLargeFileWrite(ctx: PolicyContext): PolicyResult {
|
|
386
|
-
if (ctx.toolName !== "Write") return allow();
|
|
387
|
-
const content = ctx.toolInput?.content as string | undefined;
|
|
388
|
-
if (typeof content !== "string") return allow();
|
|
389
|
-
const thresholdKb = ((ctx.params?.thresholdKb ?? 1024) as number);
|
|
390
|
-
const thresholdBytes = thresholdKb * 1024;
|
|
391
|
-
if (content.length > thresholdBytes) {
|
|
392
|
-
return instruct(
|
|
393
|
-
`STOP: You are writing a file larger than ${thresholdKb}KB (${Math.round(content.length / 1024)}KB). This is unusually large. Confirm this is intentional before proceeding.`,
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
|
-
return allow();
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function warnPackagePublish(ctx: PolicyContext): PolicyResult {
|
|
400
|
-
if (ctx.toolName !== "Bash") return allow();
|
|
401
|
-
const cmd = getCommand(ctx);
|
|
402
|
-
if (PUBLISH_CMD_RE.test(cmd)) {
|
|
403
|
-
return instruct(
|
|
404
|
-
"STOP: This command publishes a package to a public registry. Confirm with the user that this is intentional.",
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
return allow();
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function protectEnvVars(ctx: PolicyContext): PolicyResult {
|
|
411
|
-
if (ctx.toolName !== "Bash") return allow();
|
|
412
|
-
const cmd = getCommand(ctx);
|
|
413
|
-
// Block: env, printenv, echo $VAR, export VAR=
|
|
414
|
-
if (ENV_PRINTENV_RE.test(cmd)) {
|
|
415
|
-
return deny("Command reads environment variables");
|
|
416
|
-
}
|
|
417
|
-
if (ECHO_ENV_RE.test(cmd)) {
|
|
418
|
-
return deny("Command echoes environment variable");
|
|
419
|
-
}
|
|
420
|
-
if (EXPORT_RE.test(cmd)) {
|
|
421
|
-
return deny("Command exports environment variable");
|
|
422
|
-
}
|
|
423
|
-
// PowerShell: $env:VAR
|
|
424
|
-
if (PS_ENV_VAR_RE.test(cmd)) {
|
|
425
|
-
return deny("Command reads environment variable via PowerShell");
|
|
426
|
-
}
|
|
427
|
-
// PowerShell: Get-ChildItem Env: / dir env: / gci env: / ls env:
|
|
428
|
-
if (PS_CHILDITEM_ENV_RE.test(cmd)) {
|
|
429
|
-
return deny("Command reads environment variables via PowerShell");
|
|
430
|
-
}
|
|
431
|
-
// PowerShell: [Environment]::GetEnvironmentVariable
|
|
432
|
-
if (DOTNET_GETENV_RE.test(cmd)) {
|
|
433
|
-
return deny("Command reads environment variable via .NET");
|
|
434
|
-
}
|
|
435
|
-
// cmd: echo %VAR%
|
|
436
|
-
if (CMD_ECHO_ENV_RE.test(cmd)) {
|
|
437
|
-
return deny("Command echoes environment variable via cmd");
|
|
438
|
-
}
|
|
439
|
-
return allow();
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function blockEnvFiles(ctx: PolicyContext): PolicyResult {
|
|
443
|
-
const cmd = getCommand(ctx);
|
|
444
|
-
const filePath = getFilePath(ctx);
|
|
445
|
-
|
|
446
|
-
// Check file_path for Read/Write tools (match both / and \ path separators)
|
|
447
|
-
if (filePath && ENV_FILE_PATH_RE.test(filePath)) {
|
|
448
|
-
return deny("Access to .env file blocked");
|
|
449
|
-
}
|
|
450
|
-
// Check Bash commands referencing .env files
|
|
451
|
-
if (ctx.toolName === "Bash" && ENV_CMD_RE.test(cmd)) {
|
|
452
|
-
return deny("Command references .env file");
|
|
453
|
-
}
|
|
454
|
-
return allow();
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function blockSudo(ctx: PolicyContext): PolicyResult {
|
|
458
|
-
if (ctx.toolName !== "Bash") return allow();
|
|
459
|
-
const cmd = getCommand(ctx).trimStart();
|
|
460
|
-
if (SUDO_RE.test(cmd) || cmd.startsWith("sudo ")) {
|
|
461
|
-
// Check allowPatterns — match against parsed tokens, not raw string
|
|
462
|
-
const allowPatterns = ((ctx.params?.allowPatterns ?? []) as string[]);
|
|
463
|
-
if (allowPatterns.some((p) => matchesAllowedPattern(cmd, p))) return allow();
|
|
464
|
-
return deny("sudo commands are blocked");
|
|
465
|
-
}
|
|
466
|
-
// PowerShell: Start-Process -Verb RunAs (elevation)
|
|
467
|
-
if (PS_ELEVATION_RE.test(cmd)) {
|
|
468
|
-
return deny("Elevated process launch is blocked");
|
|
469
|
-
}
|
|
470
|
-
// Windows: runas command
|
|
471
|
-
if (RUNAS_RE.test(cmd)) {
|
|
472
|
-
return deny("runas elevation is blocked");
|
|
473
|
-
}
|
|
474
|
-
return allow();
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function blockCurlPipeSh(ctx: PolicyContext): PolicyResult {
|
|
478
|
-
if (ctx.toolName !== "Bash") return allow();
|
|
479
|
-
const cmd = getCommand(ctx);
|
|
480
|
-
if (CURL_PIPE_SH_RE.test(cmd)) {
|
|
481
|
-
return deny("Piping downloads to shell is blocked");
|
|
482
|
-
}
|
|
483
|
-
// PowerShell: iwr | iex, irm | iex, Invoke-WebRequest | Invoke-Expression
|
|
484
|
-
if (PS_WEB_PIPE_RE.test(cmd)) {
|
|
485
|
-
return deny("Piping downloads to Invoke-Expression is blocked");
|
|
486
|
-
}
|
|
487
|
-
return allow();
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function extractGitPushArgs(cmd: string): string[] {
|
|
491
|
-
return cmd
|
|
492
|
-
.split(/&&|\|\||[|;\n]/)
|
|
493
|
-
.map((s) => s.trim())
|
|
494
|
-
.filter((s) => /^git\s+push\s/.test(s))
|
|
495
|
-
.map((s) => s.replace(/^git\s+push\s+/, ""));
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function blockPushMaster(ctx: PolicyContext): PolicyResult {
|
|
499
|
-
if (ctx.toolName !== "Bash") return allow();
|
|
500
|
-
const protectedBranches = ((ctx.params?.protectedBranches ?? ["main", "master"]) as string[]);
|
|
501
|
-
if (protectedBranches.length === 0) return allow();
|
|
502
|
-
const args = extractGitPushArgs(getCommand(ctx));
|
|
503
|
-
const branchPattern = new RegExp(`\\b(?:${protectedBranches.map((b) => b.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`);
|
|
504
|
-
if (args.some((a) => branchPattern.test(a))) {
|
|
505
|
-
return deny(`Pushing to ${protectedBranches.join("/")} is blocked`);
|
|
506
|
-
}
|
|
507
|
-
return allow();
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Check whether all *recursive* rm path targets in a command are under an allowlisted path.
|
|
512
|
-
* Splits on shell operators first so that `/tmp` appearing in an unrelated
|
|
513
|
-
* sub-command (e.g. `echo /tmp && rm -rf /`) does not trigger a false allow.
|
|
514
|
-
* Uses path-boundary comparison so `/tmp` does not cover `/tmp2`.
|
|
515
|
-
* Non-recursive rm segments (no -r/-R flag) are skipped — they pose no catastrophic risk.
|
|
516
|
-
* Quoted paths with spaces are handled via a segment-level regex fallback.
|
|
517
|
-
*/
|
|
518
|
-
function rmTargetIsAllowed(cmd: string, allowPaths: string[]): boolean {
|
|
519
|
-
if (allowPaths.length === 0) return false;
|
|
520
|
-
const segments = cmd
|
|
521
|
-
.split(/&&|\|\||[|;\n]/)
|
|
522
|
-
.map((s) => s.trim())
|
|
523
|
-
.filter((s) => /\brm\b/.test(s));
|
|
524
|
-
if (segments.length === 0) return false;
|
|
525
|
-
for (const seg of segments) {
|
|
526
|
-
const tokens = parseArgvTokens(seg);
|
|
527
|
-
const rmIdx = tokens.findIndex((t) => t === "rm");
|
|
528
|
-
if (rmIdx < 0) continue;
|
|
529
|
-
// Only validate recursive rm segments — non-recursive rm has no catastrophic-deletion risk
|
|
530
|
-
const flagTokens = tokens.slice(rmIdx + 1).filter((t) => /^-[^-]/.test(t));
|
|
531
|
-
const longFlagsInSeg = tokens.slice(rmIdx + 1).filter((t) => /^--/.test(t));
|
|
532
|
-
if (!/r/i.test(flagTokens.join("")) && !longFlagsInSeg.some(f => /^--recursive$/i.test(f))) continue;
|
|
533
|
-
const pathArgs = tokens.slice(rmIdx + 1).filter((t) => !t.startsWith("-"));
|
|
534
|
-
for (const target of pathArgs) {
|
|
535
|
-
const normalized = target.replace(/\/\*$/, "").replace(/\/+$/, "") || "/";
|
|
536
|
-
const covered = allowPaths.some((p) => {
|
|
537
|
-
const np = p.replace(/\/+$/, "") || "/";
|
|
538
|
-
return normalized === np || normalized.startsWith(np + "/");
|
|
539
|
-
});
|
|
540
|
-
if (!covered) {
|
|
541
|
-
// Fallback: check the raw segment for quoted paths that contain spaces
|
|
542
|
-
// (parseArgvTokens splits on whitespace, so "/tmp/my dir" becomes two tokens)
|
|
543
|
-
const segCovered = allowPaths.some((p) => {
|
|
544
|
-
const escaped = p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
545
|
-
return new RegExp(`${escaped}(?:[/"'\\s/*]|$)`).test(seg);
|
|
546
|
-
});
|
|
547
|
-
if (!segCovered) return false;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
return true;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function blockRmRf(ctx: PolicyContext): PolicyResult {
|
|
555
|
-
if (ctx.toolName !== "Bash") return allow();
|
|
556
|
-
const cmd = getCommand(ctx);
|
|
557
|
-
const hasDestructivePath = parseArgvTokens(cmd).some((token) => {
|
|
558
|
-
const normalized = token.replace(/\/\*$/, "").replace(/\/+$/, "") || (token.startsWith("/") ? "/" : "");
|
|
559
|
-
return normalized === "/" || normalized === "~" || /^\/[A-Za-z_][\w.-]*$/.test(normalized);
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
// Combined flags in one token: rm -rf /, rm -fr /
|
|
563
|
-
if (hasDestructivePath && (
|
|
564
|
-
/rm\s+-[^\s]*r[^\s]*f[^\s]*/.test(cmd) ||
|
|
565
|
-
/rm\s+-[^\s]*f[^\s]*r[^\s]*/.test(cmd)
|
|
566
|
-
)) {
|
|
567
|
-
const allowPaths = ((ctx.params?.allowPaths ?? []) as string[]);
|
|
568
|
-
if (rmTargetIsAllowed(cmd, allowPaths)) return allow();
|
|
569
|
-
return deny("Catastrophic deletion blocked");
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// Separated flags: rm -r -f /, rm -f -r /, rm -r -v -f /, etc.
|
|
573
|
-
if (hasDestructivePath && /\brm\b/.test(cmd)) {
|
|
574
|
-
const tokens = parseArgvTokens(cmd);
|
|
575
|
-
const shortFlags = tokens.filter((t) => /^-[^-]/.test(t)).join("");
|
|
576
|
-
const longFlags = tokens.filter((t) => /^--/.test(t));
|
|
577
|
-
const hasRecursive = /r/i.test(shortFlags) || longFlags.some(f => /^--recursive$/i.test(f));
|
|
578
|
-
const hasForce = /f/.test(shortFlags) || longFlags.some(f => /^--force$/i.test(f));
|
|
579
|
-
if (hasRecursive && hasForce) {
|
|
580
|
-
const allowPaths = ((ctx.params?.allowPaths ?? []) as string[]);
|
|
581
|
-
if (rmTargetIsAllowed(cmd, allowPaths)) return allow();
|
|
582
|
-
return deny("Catastrophic deletion blocked");
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
// PowerShell: Remove-Item -Recurse -Force on root/drive
|
|
586
|
-
if (/Remove-Item\s+.*-Recurse.*-Force.*(?:[A-Z]:\\(?:\s|$)|\\\*)/i.test(cmd)) {
|
|
587
|
-
return deny("Catastrophic deletion blocked");
|
|
588
|
-
}
|
|
589
|
-
// cmd: rd /s /q or rmdir /s /q on drive root
|
|
590
|
-
if (/(?:rd|rmdir)\s+\/s\s+\/q\s+[A-Z]:\\/i.test(cmd)) {
|
|
591
|
-
return deny("Catastrophic deletion blocked");
|
|
592
|
-
}
|
|
593
|
-
return allow();
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function blockForcePush(ctx: PolicyContext): PolicyResult {
|
|
597
|
-
if (ctx.toolName !== "Bash") return allow();
|
|
598
|
-
const args = extractGitPushArgs(getCommand(ctx));
|
|
599
|
-
if (args.some((a) => FORCE_PUSH_RE.test(a))) {
|
|
600
|
-
return deny("Force-pushing is blocked");
|
|
601
|
-
}
|
|
602
|
-
return allow();
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
function blockSecretsWrite(ctx: PolicyContext): PolicyResult {
|
|
606
|
-
if (ctx.toolName !== "Write") return allow();
|
|
607
|
-
const filePath = getFilePath(ctx);
|
|
608
|
-
if (SECRET_FILE_RE.test(filePath) || SECRET_FILE_ID_RSA_RE.test(filePath) || SECRET_FILE_CREDENTIALS_RE.test(filePath)) {
|
|
609
|
-
return deny("Writing secret key files is blocked");
|
|
610
|
-
}
|
|
611
|
-
const additionalPatterns = ((ctx.params?.additionalPatterns ?? []) as string[]);
|
|
612
|
-
for (const pattern of additionalPatterns) {
|
|
613
|
-
if (filePath.includes(pattern)) {
|
|
614
|
-
return deny(`Writing blocked file pattern: ${pattern}`);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
return allow();
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/** Read-like commands that access file system contents. */
|
|
621
|
-
const READ_LIKE_CMDS =
|
|
622
|
-
/(?:^|;|&&|\|\||\|)\s*(?:ls|find|cat|head|tail|less|more|wc|file|stat|tree|du)\s/;
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* Extract absolute paths from a Bash command string.
|
|
626
|
-
* Scans quoted strings only in the first pipeline segment (before the first
|
|
627
|
-
* bare pipe) and only when the quoted content has no glob or regex metacharacters.
|
|
628
|
-
* This catches `cat "/etc/passwd"` while avoiding false positives from grep
|
|
629
|
-
* patterns and find glob patterns that appear in later pipeline stages.
|
|
630
|
-
* Unquoted absolute paths are extracted from the whole command as before.
|
|
631
|
-
*/
|
|
632
|
-
function extractAbsolutePaths(command: string): string[] {
|
|
633
|
-
const paths: string[] = [];
|
|
634
|
-
const pathRe = /(?<![a-zA-Z0-9_.\-~\\])(?:~\/[^\s;|&"'()\[\]{}]*|~(?=\s|$|[;|&"'()\[\]{}])|\/[^\s;|&"'()\[\]{}]*)/g;
|
|
635
|
-
|
|
636
|
-
function addPaths(s: string): void {
|
|
637
|
-
pathRe.lastIndex = 0;
|
|
638
|
-
let m: RegExpExecArray | null;
|
|
639
|
-
while ((m = pathRe.exec(s)) !== null) {
|
|
640
|
-
let p = m[0];
|
|
641
|
-
if (p === "~") p = homedir();
|
|
642
|
-
else if (p.startsWith("~/")) p = join(homedir(), p.slice(2));
|
|
643
|
-
paths.push(p);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// Find the index of the first bare pipe (not inside quotes) to limit quoted extraction.
|
|
648
|
-
let firstBarePipe = command.length;
|
|
649
|
-
let inDouble = false, inSingle = false;
|
|
650
|
-
for (let i = 0; i < command.length; i++) {
|
|
651
|
-
const c = command[i];
|
|
652
|
-
if (c === '"' && !inSingle) inDouble = !inDouble;
|
|
653
|
-
else if (c === "'" && !inDouble) inSingle = !inSingle;
|
|
654
|
-
else if (c === "|" && !inDouble && !inSingle) { firstBarePipe = i; break; }
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// Extract paths from quoted strings in the FIRST pipeline segment only,
|
|
658
|
-
// and only when the content has no glob/regex metacharacters.
|
|
659
|
-
const firstSegment = command.slice(0, firstBarePipe);
|
|
660
|
-
const quotedRe = /"([^"]*)"|'([^']*)'/g;
|
|
661
|
-
let qm: RegExpExecArray | null;
|
|
662
|
-
while ((qm = quotedRe.exec(firstSegment)) !== null) {
|
|
663
|
-
const content = qm[1] ?? qm[2] ?? "";
|
|
664
|
-
// Skip patterns that contain glob or common regex metacharacters
|
|
665
|
-
if (/[*?\[\]^$+()\\]/.test(content)) continue;
|
|
666
|
-
addPaths(content);
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// Extract from unquoted portions of the whole command (existing behaviour).
|
|
670
|
-
const stripped = command
|
|
671
|
-
.replace(/"[^"]*"/g, (m) => " ".repeat(m.length))
|
|
672
|
-
.replace(/'[^']*'/g, (m) => " ".repeat(m.length));
|
|
673
|
-
addPaths(stripped);
|
|
674
|
-
|
|
675
|
-
return paths;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
function blockReadOutsideCwd(ctx: PolicyContext): PolicyResult {
|
|
679
|
-
const cwd = ctx.session?.cwd;
|
|
680
|
-
if (!cwd) return allow(); // Can't enforce without cwd
|
|
681
|
-
|
|
682
|
-
const allowPaths = ((ctx.params?.allowPaths ?? []) as string[]);
|
|
683
|
-
|
|
684
|
-
// For Bash tool: check read-like commands for absolute paths outside cwd
|
|
685
|
-
if (ctx.toolName === "Bash") {
|
|
686
|
-
const cmd = getCommand(ctx);
|
|
687
|
-
if (!READ_LIKE_CMDS.test(cmd)) return allow();
|
|
688
|
-
|
|
689
|
-
const paths = extractAbsolutePaths(cmd);
|
|
690
|
-
const cwdWithSep = cwd.endsWith("/") ? cwd : cwd + "/";
|
|
691
|
-
for (const p of paths) {
|
|
692
|
-
const resolved = resolve(cwd, p);
|
|
693
|
-
if (isClaudeSettingsFile(resolved)) {
|
|
694
|
-
return deny(`Reading Claude settings file blocked: ${resolved}`);
|
|
695
|
-
}
|
|
696
|
-
if (isClaudeInternalPath(resolved)) continue; // Whitelist ~/.claude/
|
|
697
|
-
if (resolved === "/dev/null") continue; // Harmless special file
|
|
698
|
-
if (resolved !== cwd && !resolved.startsWith(cwdWithSep)) {
|
|
699
|
-
if (allowPaths.some((ap) => resolved === ap || resolved.startsWith(ap.endsWith("/") ? ap : ap + "/"))) continue;
|
|
700
|
-
return deny(`Bash read outside project directory blocked: ${resolved}`);
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
return allow();
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// For Read/Glob/Grep: existing file_path / path check
|
|
707
|
-
const filePath = getFilePath(ctx);
|
|
708
|
-
const searchPath = (ctx.toolInput?.path as string) ?? "";
|
|
709
|
-
|
|
710
|
-
const target = filePath || searchPath;
|
|
711
|
-
if (!target) return allow();
|
|
712
|
-
|
|
713
|
-
const resolved = resolve(cwd, target);
|
|
714
|
-
|
|
715
|
-
// Block settings files in any .claude directory before whitelisting
|
|
716
|
-
if (isClaudeSettingsFile(resolved)) {
|
|
717
|
-
return deny(`Reading Claude settings file blocked: ${resolved}`);
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
// Whitelist ~/.claude/ — Claude Code's own config, plans, memory, and settings
|
|
721
|
-
if (isClaudeInternalPath(resolved)) return allow();
|
|
722
|
-
|
|
723
|
-
// Whitelist /dev/null — harmless special file commonly used in shell commands
|
|
724
|
-
if (resolved === "/dev/null") return allow();
|
|
725
|
-
|
|
726
|
-
const cwdWithSep = cwd.endsWith("/") ? cwd : cwd + "/";
|
|
727
|
-
if (resolved !== cwd && !resolved.startsWith(cwdWithSep)) {
|
|
728
|
-
if (allowPaths.some((ap) => resolved === ap || resolved.startsWith(ap.endsWith("/") ? ap : ap + "/"))) return allow();
|
|
729
|
-
return deny(`Access outside project directory blocked: ${resolved}`);
|
|
730
|
-
}
|
|
731
|
-
return allow();
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
function blockWorkOnMain(ctx: PolicyContext): PolicyResult {
|
|
735
|
-
if (ctx.toolName !== "Bash") return allow();
|
|
736
|
-
const cmd = getCommand(ctx);
|
|
737
|
-
if (!GIT_COMMIT_MERGE_RE.test(cmd)) return allow();
|
|
738
|
-
|
|
739
|
-
const cwd = ctx.session?.cwd;
|
|
740
|
-
if (!cwd) return allow();
|
|
741
|
-
|
|
742
|
-
const branch = getCurrentBranch(cwd);
|
|
743
|
-
if (!branch) return allow();
|
|
744
|
-
|
|
745
|
-
const protectedBranches = ((ctx.params?.protectedBranches ?? ["main", "master"]) as string[]);
|
|
746
|
-
if (protectedBranches.includes(branch)) {
|
|
747
|
-
return deny(
|
|
748
|
-
`Git ${cmd.match(/git\s+(\S+)/)?.[1] ?? "operation"} on ${branch} is blocked. Create a feature branch first.`,
|
|
749
|
-
);
|
|
750
|
-
}
|
|
751
|
-
return allow();
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
function blockFailproofaiCommands(ctx: PolicyContext): PolicyResult {
|
|
755
|
-
if (ctx.toolName !== "Bash") return allow();
|
|
756
|
-
const cmd = getCommand(ctx);
|
|
757
|
-
|
|
758
|
-
// Block direct failproofai CLI invocations
|
|
759
|
-
if (FAILPROOFAI_CLI_RE.test(cmd)) {
|
|
760
|
-
return deny("Running failproofai CLI commands is blocked");
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
// Block package-manager uninstallation of failproofai
|
|
764
|
-
if (FAILPROOFAI_UNINSTALL_RE.test(cmd)) {
|
|
765
|
-
return deny("Uninstalling failproofai is blocked");
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
return allow();
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
// Maximum size of the per-session tool-call sidecar before we stop updating it.
|
|
772
|
-
// If exceeded, repeated-call detection degrades gracefully (allows through) rather
|
|
773
|
-
// than growing the file unboundedly.
|
|
774
|
-
const TOOL_CALL_TRACKER_MAX_BYTES = 65_536; // 64 KB
|
|
775
|
-
|
|
776
|
-
async function warnRepeatedToolCalls(ctx: PolicyContext): Promise<PolicyResult> {
|
|
777
|
-
const THRESHOLD = 3;
|
|
778
|
-
const transcriptPath = ctx.session?.transcriptPath;
|
|
779
|
-
if (!transcriptPath || !ctx.toolName || !ctx.toolInput) return allow();
|
|
780
|
-
|
|
781
|
-
// Sidecar file tracks { fingerprint: count } — O(1) per call vs O(transcript) per call.
|
|
782
|
-
const trackerPath = `${transcriptPath}.tool-calls.json`;
|
|
783
|
-
const fingerprint = JSON.stringify({ tool: ctx.toolName, input: ctx.toolInput });
|
|
784
|
-
|
|
785
|
-
let counts: Record<string, number> = {};
|
|
786
|
-
try {
|
|
787
|
-
const raw = await readFile(trackerPath, "utf8");
|
|
788
|
-
counts = JSON.parse(raw) as Record<string, number>;
|
|
789
|
-
} catch { /* first call or unreadable — start fresh */ }
|
|
790
|
-
|
|
791
|
-
const prevCount = counts[fingerprint] ?? 0;
|
|
792
|
-
if (prevCount >= THRESHOLD) {
|
|
793
|
-
return instruct(
|
|
794
|
-
`STOP: You have already called ${ctx.toolName} ${prevCount} times with identical parameters. This is wasteful and unproductive. Do NOT repeat this call — use a different approach or ask the user for clarification.`,
|
|
795
|
-
);
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
counts[fingerprint] = prevCount + 1;
|
|
799
|
-
try {
|
|
800
|
-
const serialized = JSON.stringify(counts);
|
|
801
|
-
if (serialized.length <= TOOL_CALL_TRACKER_MAX_BYTES) {
|
|
802
|
-
await writeFile(trackerPath, serialized, "utf8");
|
|
803
|
-
}
|
|
804
|
-
} catch { /* non-fatal */ }
|
|
805
|
-
|
|
806
|
-
return allow();
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
function warnGitAmend(ctx: PolicyContext): PolicyResult {
|
|
810
|
-
if (ctx.toolName !== "Bash") return allow();
|
|
811
|
-
const cmd = getCommand(ctx);
|
|
812
|
-
if (GIT_AMEND_RE.test(cmd)) {
|
|
813
|
-
return instruct(
|
|
814
|
-
"STOP: This command amends the last commit, which rewrites git history. If this commit has already been pushed to a shared branch, this will cause divergence for other contributors. Confirm with the user before executing.",
|
|
815
|
-
);
|
|
816
|
-
}
|
|
817
|
-
return allow();
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
function warnGitStashDrop(ctx: PolicyContext): PolicyResult {
|
|
821
|
-
if (ctx.toolName !== "Bash") return allow();
|
|
822
|
-
const cmd = getCommand(ctx);
|
|
823
|
-
if (GIT_STASH_DROP_RE.test(cmd)) {
|
|
824
|
-
return instruct(
|
|
825
|
-
"STOP: This command permanently deletes stashed changes (git stash drop/clear). Stash entries cannot be recovered after deletion. Confirm with the user before executing.",
|
|
826
|
-
);
|
|
827
|
-
}
|
|
828
|
-
return allow();
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
function warnAllFilesStaged(ctx: PolicyContext): PolicyResult {
|
|
832
|
-
if (ctx.toolName !== "Bash") return allow();
|
|
833
|
-
const cmd = getCommand(ctx);
|
|
834
|
-
if (GIT_ADD_ALL_RE.test(cmd)) {
|
|
835
|
-
return instruct(
|
|
836
|
-
"STOP: This command stages all files in the working tree (git add -A / --all / .). This may inadvertently include build artifacts, generated files, or sensitive files not covered by .gitignore. Confirm with the user before executing.",
|
|
837
|
-
);
|
|
838
|
-
}
|
|
839
|
-
return allow();
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
function warnSchemaAlteration(ctx: PolicyContext): PolicyResult {
|
|
843
|
-
if (ctx.toolName !== "Bash") return allow();
|
|
844
|
-
const cmd = getCommand(ctx);
|
|
845
|
-
if (!SQL_TOOL_RE.test(cmd)) return allow();
|
|
846
|
-
if (SCHEMA_ALTER_RE.test(cmd)) {
|
|
847
|
-
return instruct(
|
|
848
|
-
"STOP: This command contains a schema-altering SQL statement (ALTER TABLE with column or rename operation). Schema changes on production databases are irreversible or disruptive. Confirm with the user before executing.",
|
|
849
|
-
);
|
|
850
|
-
}
|
|
851
|
-
return allow();
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
function warnGlobalPackageInstall(ctx: PolicyContext): PolicyResult {
|
|
855
|
-
if (ctx.toolName !== "Bash") return allow();
|
|
856
|
-
const cmd = getCommand(ctx);
|
|
857
|
-
const isGlobal =
|
|
858
|
-
NPM_GLOBAL_RE.test(cmd) ||
|
|
859
|
-
YARN_GLOBAL_RE.test(cmd) ||
|
|
860
|
-
PNPM_GLOBAL_RE.test(cmd) ||
|
|
861
|
-
BUN_GLOBAL_RE.test(cmd) ||
|
|
862
|
-
CARGO_INSTALL_RE.test(cmd) ||
|
|
863
|
-
// Bare 'pip install' respects the active venv when one is present;
|
|
864
|
-
// only flag explicit system-level flags (--user, --break-system-packages).
|
|
865
|
-
PIP_SYSTEM_RE.test(cmd);
|
|
866
|
-
if (isGlobal) {
|
|
867
|
-
return instruct(
|
|
868
|
-
"STOP: This command installs a package globally, which modifies the system-wide environment outside the project. This can conflict with other projects or system tools. Confirm with the user before executing.",
|
|
869
|
-
);
|
|
870
|
-
}
|
|
871
|
-
return allow();
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
// Split a compound shell command into independent segments.
|
|
875
|
-
const SEGMENT_SPLIT_RE = /\s*(?:&&|\|\||\||;)\s*/;
|
|
876
|
-
|
|
877
|
-
function preferPackageManager(ctx: PolicyContext): PolicyResult {
|
|
878
|
-
if (ctx.toolName !== "Bash") return allow();
|
|
879
|
-
const cmd = getCommand(ctx);
|
|
880
|
-
if (!cmd) return allow();
|
|
881
|
-
|
|
882
|
-
const allowed = (ctx.params?.allowed ?? []) as string[];
|
|
883
|
-
if (allowed.length === 0) return allow();
|
|
884
|
-
|
|
885
|
-
const allowedSet = new Set(allowed.map((a) => a.toLowerCase()));
|
|
886
|
-
const blocked = (ctx.params?.blocked ?? []) as string[];
|
|
887
|
-
const allowedList = allowed.join(", ");
|
|
888
|
-
|
|
889
|
-
// Evaluate each shell segment independently so that
|
|
890
|
-
// "uv --version && pip install flask" correctly denies the pip segment.
|
|
891
|
-
const segments = cmd.split(SEGMENT_SPLIT_RE);
|
|
892
|
-
|
|
893
|
-
for (const segment of segments) {
|
|
894
|
-
const trimmed = segment.trim();
|
|
895
|
-
if (!trimmed) continue;
|
|
896
|
-
|
|
897
|
-
// Check if this segment uses an allowed manager — if so, skip it.
|
|
898
|
-
let segmentAllowed = false;
|
|
899
|
-
for (const manager of allowedSet) {
|
|
900
|
-
const patterns = PKG_MANAGER_DETECTORS[manager];
|
|
901
|
-
if (!patterns) continue;
|
|
902
|
-
for (const pattern of patterns) {
|
|
903
|
-
if (pattern.test(trimmed)) { segmentAllowed = true; break; }
|
|
904
|
-
}
|
|
905
|
-
if (segmentAllowed) break;
|
|
906
|
-
}
|
|
907
|
-
if (segmentAllowed) continue;
|
|
908
|
-
|
|
909
|
-
// Check if this segment uses a non-allowed builtin manager.
|
|
910
|
-
for (const [manager, patterns] of Object.entries(PKG_MANAGER_DETECTORS)) {
|
|
911
|
-
if (allowedSet.has(manager)) continue;
|
|
912
|
-
for (const pattern of patterns) {
|
|
913
|
-
if (pattern.test(trimmed)) {
|
|
914
|
-
return deny(
|
|
915
|
-
`"${manager}" is not an allowed package manager. ` +
|
|
916
|
-
`Allowed package managers for this project: ${allowedList}. ` +
|
|
917
|
-
`Rewrite this command using an allowed package manager.`,
|
|
918
|
-
);
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
// Check user-specified blocked managers.
|
|
924
|
-
for (const name of blocked) {
|
|
925
|
-
const lower = name.toLowerCase();
|
|
926
|
-
if (allowedSet.has(lower)) continue;
|
|
927
|
-
const re = new RegExp(`\\b${lower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
|
|
928
|
-
if (re.test(trimmed)) {
|
|
929
|
-
return deny(
|
|
930
|
-
`"${lower}" is not an allowed package manager. ` +
|
|
931
|
-
`Allowed package managers for this project: ${allowedList}. ` +
|
|
932
|
-
`Rewrite this command using an allowed package manager.`,
|
|
933
|
-
);
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
return allow();
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
function warnBackgroundProcess(ctx: PolicyContext): PolicyResult {
|
|
942
|
-
if (ctx.toolName !== "Bash") return allow();
|
|
943
|
-
const cmd = getCommand(ctx);
|
|
944
|
-
const isBackground =
|
|
945
|
-
NOHUP_RE.test(cmd) ||
|
|
946
|
-
SCREEN_DETACH_RE.test(cmd) ||
|
|
947
|
-
TMUX_DETACH_RE.test(cmd) ||
|
|
948
|
-
DISOWN_RE.test(cmd) ||
|
|
949
|
-
BACKGROUND_AMPERSAND_RE.test(cmd);
|
|
950
|
-
if (isBackground) {
|
|
951
|
-
return instruct(
|
|
952
|
-
"STOP: This command starts a background or detached process (nohup, screen -d, tmux -d, or trailing &). Background processes persist after Claude's session and may be difficult to track or stop. Confirm with the user before executing.",
|
|
953
|
-
);
|
|
954
|
-
}
|
|
955
|
-
return allow();
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
// -- Workflow (Stop event) policies --
|
|
959
|
-
|
|
960
|
-
function requireCommitBeforeStop(ctx: PolicyContext): PolicyResult {
|
|
961
|
-
const cwd = ctx.session?.cwd;
|
|
962
|
-
if (!cwd) return allow("No working directory available, skipping commit check.");
|
|
963
|
-
|
|
964
|
-
try {
|
|
965
|
-
const status = execSync("git status --porcelain", {
|
|
966
|
-
cwd,
|
|
967
|
-
encoding: "utf8",
|
|
968
|
-
timeout: 5000,
|
|
969
|
-
}).trim();
|
|
970
|
-
|
|
971
|
-
if (status.length > 0) {
|
|
972
|
-
return deny(
|
|
973
|
-
"You have uncommitted changes in the working directory. Commit all changes now.",
|
|
974
|
-
);
|
|
975
|
-
}
|
|
976
|
-
return allow("All changes are committed.");
|
|
977
|
-
} catch {
|
|
978
|
-
return allow("Not a git repository, skipping commit check.");
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
function requirePushBeforeStop(ctx: PolicyContext): PolicyResult {
|
|
983
|
-
const cwd = ctx.session?.cwd;
|
|
984
|
-
if (!cwd) return allow("No working directory available, skipping push check.");
|
|
985
|
-
|
|
986
|
-
try {
|
|
987
|
-
const remotes = execSync("git remote", {
|
|
988
|
-
cwd,
|
|
989
|
-
encoding: "utf8",
|
|
990
|
-
timeout: 3000,
|
|
991
|
-
}).trim();
|
|
992
|
-
|
|
993
|
-
if (!remotes) return allow("No git remote configured, skipping push check.");
|
|
994
|
-
|
|
995
|
-
const remote = (ctx.params?.remote as string) ?? "origin";
|
|
996
|
-
|
|
997
|
-
const branch = getCurrentBranch(cwd);
|
|
998
|
-
if (!branch || branch === "HEAD") return allow("Detached HEAD, skipping push check.");
|
|
999
|
-
|
|
1000
|
-
const baseBranch = (ctx.params?.baseBranch as string) ?? "main";
|
|
1001
|
-
|
|
1002
|
-
// If on the base branch itself, no push of a feature branch is needed
|
|
1003
|
-
if (branch === baseBranch) {
|
|
1004
|
-
return allow(`On base branch "${baseBranch}", skipping push check.`);
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
// Check if branch has diverged from base in any meaningful way
|
|
1008
|
-
try {
|
|
1009
|
-
const ahead = execFileSync(
|
|
1010
|
-
"git",
|
|
1011
|
-
["log", `${remote}/${baseBranch}..HEAD`, "--oneline"],
|
|
1012
|
-
{ cwd, encoding: "utf8", timeout: 5000 },
|
|
1013
|
-
).trim();
|
|
1014
|
-
|
|
1015
|
-
if (!ahead) {
|
|
1016
|
-
// No commits ahead — branch is fully merged (regular merge / fast-forward)
|
|
1017
|
-
return allow(`No commits ahead of ${remote}/${baseBranch}, skipping push check.`);
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
// Commits exist but might be from a squash-merged PR.
|
|
1021
|
-
// Check actual file diff — if trees are identical, work is already in base.
|
|
1022
|
-
const diff = execFileSync(
|
|
1023
|
-
"git",
|
|
1024
|
-
["diff", "--stat", `${remote}/${baseBranch}`, "HEAD"],
|
|
1025
|
-
{ cwd, encoding: "utf8", timeout: 5000 },
|
|
1026
|
-
).trim();
|
|
1027
|
-
|
|
1028
|
-
if (!diff) {
|
|
1029
|
-
return allow(`No file changes compared to ${remote}/${baseBranch}, skipping push check.`);
|
|
1030
|
-
}
|
|
1031
|
-
} catch {
|
|
1032
|
-
// remote/{baseBranch} ref missing — fall through to existing push checks
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
// Check if remote tracking branch exists
|
|
1036
|
-
let hasTracking = false;
|
|
1037
|
-
try {
|
|
1038
|
-
execFileSync("git", ["rev-parse", "--verify", `${remote}/${branch}`], {
|
|
1039
|
-
cwd,
|
|
1040
|
-
encoding: "utf8",
|
|
1041
|
-
timeout: 3000,
|
|
1042
|
-
});
|
|
1043
|
-
hasTracking = true;
|
|
1044
|
-
} catch {
|
|
1045
|
-
// Remote tracking branch does not exist
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
if (!hasTracking) {
|
|
1049
|
-
return deny(
|
|
1050
|
-
`Branch "${branch}" has not been pushed to remote "${remote}". ` +
|
|
1051
|
-
`Run now: git push -u ${remote} ${branch}`,
|
|
1052
|
-
);
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
// Check for unpushed commits
|
|
1056
|
-
const unpushed = execFileSync("git", ["log", `${remote}/${branch}..HEAD`, "--oneline"], {
|
|
1057
|
-
cwd,
|
|
1058
|
-
encoding: "utf8",
|
|
1059
|
-
timeout: 5000,
|
|
1060
|
-
}).trim();
|
|
1061
|
-
|
|
1062
|
-
if (unpushed.length > 0) {
|
|
1063
|
-
const commitCount = unpushed.split("\n").length;
|
|
1064
|
-
return deny(
|
|
1065
|
-
`You have ${commitCount} unpushed commit${commitCount > 1 ? "s" : ""} on branch "${branch}". ` +
|
|
1066
|
-
`Run now: git push`,
|
|
1067
|
-
);
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
return allow(`All commits pushed to "${remote}".`);
|
|
1071
|
-
} catch {
|
|
1072
|
-
return allow("Could not check push status, skipping.");
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
function requirePrBeforeStop(ctx: PolicyContext): PolicyResult {
|
|
1077
|
-
const cwd = ctx.session?.cwd;
|
|
1078
|
-
if (!cwd) return allow("No working directory available, skipping PR check.");
|
|
1079
|
-
|
|
1080
|
-
try {
|
|
1081
|
-
// Check if gh CLI is available
|
|
1082
|
-
try {
|
|
1083
|
-
execSync("gh --version", { cwd, encoding: "utf8", timeout: 3000 });
|
|
1084
|
-
} catch {
|
|
1085
|
-
return allow("GitHub CLI (gh) not installed, skipping PR check.");
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
const branch = getCurrentBranch(cwd);
|
|
1089
|
-
if (!branch || branch === "HEAD") return allow("Detached HEAD, skipping PR check.");
|
|
1090
|
-
|
|
1091
|
-
const baseBranch = (ctx.params?.baseBranch as string) ?? "main";
|
|
1092
|
-
|
|
1093
|
-
// If on the base branch itself, no PR is needed
|
|
1094
|
-
if (branch === baseBranch) {
|
|
1095
|
-
return allow(`On base branch "${baseBranch}", skipping PR check.`);
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
// Check if branch has diverged from base in any meaningful way
|
|
1099
|
-
try {
|
|
1100
|
-
const ahead = execFileSync(
|
|
1101
|
-
"git",
|
|
1102
|
-
["log", `origin/${baseBranch}..HEAD`, "--oneline"],
|
|
1103
|
-
{ cwd, encoding: "utf8", timeout: 5000 },
|
|
1104
|
-
).trim();
|
|
1105
|
-
|
|
1106
|
-
if (!ahead) {
|
|
1107
|
-
// No commits ahead — branch is fully merged (regular merge / fast-forward)
|
|
1108
|
-
return allow(`No commits ahead of origin/${baseBranch}, skipping PR check.`);
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// Commits exist but might be from a squash-merged PR.
|
|
1112
|
-
// Check actual file diff — if trees are identical, work is already in main.
|
|
1113
|
-
const diff = execFileSync(
|
|
1114
|
-
"git",
|
|
1115
|
-
["diff", "--stat", `origin/${baseBranch}`, "HEAD"],
|
|
1116
|
-
{ cwd, encoding: "utf8", timeout: 5000 },
|
|
1117
|
-
).trim();
|
|
1118
|
-
|
|
1119
|
-
if (!diff) {
|
|
1120
|
-
return allow(`No file changes compared to origin/${baseBranch}, skipping PR check.`);
|
|
1121
|
-
}
|
|
1122
|
-
} catch {
|
|
1123
|
-
// origin/{baseBranch} ref missing or git error — fall through to gh pr view
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
// Check if a PR exists for this branch
|
|
1127
|
-
let prJson: string;
|
|
1128
|
-
try {
|
|
1129
|
-
prJson = execSync("gh pr view --json number,url,state", {
|
|
1130
|
-
cwd,
|
|
1131
|
-
encoding: "utf8",
|
|
1132
|
-
timeout: 15000,
|
|
1133
|
-
}).trim();
|
|
1134
|
-
} catch {
|
|
1135
|
-
// gh pr view exits non-zero when no PR exists
|
|
1136
|
-
return deny(
|
|
1137
|
-
`No pull request found for branch "${branch}". ` +
|
|
1138
|
-
`Run now: gh pr create`,
|
|
1139
|
-
);
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
const pr = JSON.parse(prJson) as { number: number; url: string; state: string };
|
|
1143
|
-
|
|
1144
|
-
if (pr.state === "OPEN") {
|
|
1145
|
-
return allow(`PR #${pr.number} exists: ${pr.url}`);
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
// PR is merged/closed. The earlier origin/{baseBranch} checks may have
|
|
1149
|
-
// used a stale ref. Fetch and re-verify before denying.
|
|
1150
|
-
if (pr.state === "MERGED") {
|
|
1151
|
-
try {
|
|
1152
|
-
execFileSync("git", ["fetch", "origin", `+refs/heads/${baseBranch}:refs/remotes/origin/${baseBranch}`], {
|
|
1153
|
-
cwd,
|
|
1154
|
-
encoding: "utf8",
|
|
1155
|
-
timeout: 10000,
|
|
1156
|
-
});
|
|
1157
|
-
const freshAhead = execFileSync(
|
|
1158
|
-
"git",
|
|
1159
|
-
["log", `origin/${baseBranch}..HEAD`, "--oneline"],
|
|
1160
|
-
{ cwd, encoding: "utf8", timeout: 5000 },
|
|
1161
|
-
).trim();
|
|
1162
|
-
if (!freshAhead) {
|
|
1163
|
-
return allow(`PR #${pr.number} was merged; branch is up to date with ${baseBranch}.`);
|
|
1164
|
-
}
|
|
1165
|
-
const freshDiff = execFileSync(
|
|
1166
|
-
"git",
|
|
1167
|
-
["diff", "--stat", `origin/${baseBranch}`, "HEAD"],
|
|
1168
|
-
{ cwd, encoding: "utf8", timeout: 5000 },
|
|
1169
|
-
).trim();
|
|
1170
|
-
if (!freshDiff) {
|
|
1171
|
-
return allow(`PR #${pr.number} was merged; no file changes vs ${baseBranch}.`);
|
|
1172
|
-
}
|
|
1173
|
-
} catch {
|
|
1174
|
-
// Fetch or git command failed — fall through to deny
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
return deny(
|
|
1179
|
-
`Pull request for branch "${branch}" is ${pr.state.toLowerCase()}. Run now: gh pr create`,
|
|
1180
|
-
);
|
|
1181
|
-
} catch {
|
|
1182
|
-
return allow("Could not check PR status, skipping.");
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
function requireCiGreenBeforeStop(ctx: PolicyContext): PolicyResult {
|
|
1187
|
-
const cwd = ctx.session?.cwd;
|
|
1188
|
-
if (!cwd) return allow("No working directory available, skipping CI check.");
|
|
1189
|
-
|
|
1190
|
-
try {
|
|
1191
|
-
// Check if gh CLI is available
|
|
1192
|
-
try {
|
|
1193
|
-
execSync("gh --version", { cwd, encoding: "utf8", timeout: 3000 });
|
|
1194
|
-
} catch {
|
|
1195
|
-
return allow("GitHub CLI (gh) not installed, skipping CI check.");
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
const branch = getCurrentBranch(cwd);
|
|
1199
|
-
if (!branch || branch === "HEAD") return allow("Detached HEAD, skipping CI check.");
|
|
1200
|
-
|
|
1201
|
-
// 1. GitHub Actions workflow runs
|
|
1202
|
-
let workflowRuns: CiCheck[] = [];
|
|
1203
|
-
try {
|
|
1204
|
-
const runsJson = execFileSync(
|
|
1205
|
-
"gh",
|
|
1206
|
-
["run", "list", "--branch", branch, "--limit", "5", "--json", "status,conclusion,name"],
|
|
1207
|
-
{ cwd, encoding: "utf8", timeout: 15000 },
|
|
1208
|
-
).trim();
|
|
1209
|
-
|
|
1210
|
-
if (runsJson && runsJson !== "[]") {
|
|
1211
|
-
workflowRuns = JSON.parse(runsJson) as CiCheck[];
|
|
1212
|
-
}
|
|
1213
|
-
} catch {
|
|
1214
|
-
// fail-open for workflow runs; continue to check third-party checks
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
// 2. Third-party check runs (CodeRabbit, SonarCloud, Codecov, etc.)
|
|
1218
|
-
let thirdPartyChecks: CiCheck[] = [];
|
|
1219
|
-
let commitStatuses: CiCheck[] = [];
|
|
1220
|
-
const sha = getHeadSha(cwd);
|
|
1221
|
-
if (sha) {
|
|
1222
|
-
thirdPartyChecks = getThirdPartyCheckRuns(cwd, sha);
|
|
1223
|
-
commitStatuses = getCommitStatuses(cwd, sha);
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
// 3. Merge all checks
|
|
1227
|
-
const allChecks = [...workflowRuns, ...thirdPartyChecks, ...commitStatuses];
|
|
1228
|
-
|
|
1229
|
-
if (allChecks.length === 0) return allow(`No CI runs found for branch "${branch}".`);
|
|
1230
|
-
|
|
1231
|
-
const failing = allChecks.filter(
|
|
1232
|
-
(r) =>
|
|
1233
|
-
r.status === "completed" &&
|
|
1234
|
-
r.conclusion !== "success" &&
|
|
1235
|
-
r.conclusion !== "skipped" &&
|
|
1236
|
-
r.conclusion !== "cancelled",
|
|
1237
|
-
);
|
|
1238
|
-
if (failing.length > 0) {
|
|
1239
|
-
const names = failing.map((r) => `"${r.name}"`).join(", ");
|
|
1240
|
-
return deny(
|
|
1241
|
-
`CI checks are failing on branch "${branch}": ${names}. Fix the failing checks now.`,
|
|
1242
|
-
);
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
const pending = allChecks.filter(
|
|
1246
|
-
(r) => r.status === "in_progress" || r.status === "queued" || r.status === "waiting",
|
|
1247
|
-
);
|
|
1248
|
-
if (pending.length > 0) {
|
|
1249
|
-
const names = pending.map((r) => `"${r.name}"`).join(", ");
|
|
1250
|
-
return deny(
|
|
1251
|
-
`CI checks are still running on branch "${branch}": ${names}. Wait for all checks to complete, then verify they pass.`,
|
|
1252
|
-
);
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
return allow(`All CI checks passed on branch "${branch}".`);
|
|
1256
|
-
} catch {
|
|
1257
|
-
return allow("Could not check CI status, skipping.");
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
// -- Registry --
|
|
1262
|
-
|
|
1263
|
-
export const BUILTIN_POLICIES: BuiltinPolicyDefinition[] = [
|
|
1264
|
-
{
|
|
1265
|
-
name: "sanitize-jwt",
|
|
1266
|
-
description: "Stop Claude from reading JWTs in tool responses",
|
|
1267
|
-
fn: sanitizeJwt,
|
|
1268
|
-
match: { events: ["PostToolUse"] },
|
|
1269
|
-
defaultEnabled: true,
|
|
1270
|
-
category: "Sanitize",
|
|
1271
|
-
},
|
|
1272
|
-
{
|
|
1273
|
-
name: "sanitize-api-keys",
|
|
1274
|
-
description: "Stop Claude from reading API keys (OpenAI, Anthropic, GitHub, AWS, Stripe, Google) in tool responses",
|
|
1275
|
-
fn: sanitizeApiKeys,
|
|
1276
|
-
match: { events: ["PostToolUse"] },
|
|
1277
|
-
defaultEnabled: true,
|
|
1278
|
-
category: "Sanitize",
|
|
1279
|
-
params: {
|
|
1280
|
-
additionalPatterns: {
|
|
1281
|
-
type: "pattern[]",
|
|
1282
|
-
description: "Additional API key patterns to scrub, each with { regex, label }",
|
|
1283
|
-
default: [],
|
|
1284
|
-
},
|
|
1285
|
-
} satisfies PolicyParamsSchema,
|
|
1286
|
-
},
|
|
1287
|
-
{
|
|
1288
|
-
name: "sanitize-connection-strings",
|
|
1289
|
-
description: "Stop Claude from reading database connection strings with embedded credentials in tool responses",
|
|
1290
|
-
fn: sanitizeConnectionStrings,
|
|
1291
|
-
match: { events: ["PostToolUse"] },
|
|
1292
|
-
defaultEnabled: true,
|
|
1293
|
-
category: "Sanitize",
|
|
1294
|
-
},
|
|
1295
|
-
{
|
|
1296
|
-
name: "sanitize-private-key-content",
|
|
1297
|
-
description: "Stop Claude from reading PEM private key content in tool responses",
|
|
1298
|
-
fn: sanitizePrivateKeyContent,
|
|
1299
|
-
match: { events: ["PostToolUse"] },
|
|
1300
|
-
defaultEnabled: true,
|
|
1301
|
-
category: "Sanitize",
|
|
1302
|
-
},
|
|
1303
|
-
{
|
|
1304
|
-
name: "sanitize-bearer-tokens",
|
|
1305
|
-
description: "Stop Claude from reading Authorization Bearer tokens in tool responses",
|
|
1306
|
-
fn: sanitizeBearerTokens,
|
|
1307
|
-
match: { events: ["PostToolUse"] },
|
|
1308
|
-
defaultEnabled: true,
|
|
1309
|
-
category: "Sanitize",
|
|
1310
|
-
},
|
|
1311
|
-
{
|
|
1312
|
-
name: "protect-env-vars",
|
|
1313
|
-
description: "Prevent commands that read environment variables",
|
|
1314
|
-
fn: protectEnvVars,
|
|
1315
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1316
|
-
defaultEnabled: true,
|
|
1317
|
-
category: "Environment",
|
|
1318
|
-
},
|
|
1319
|
-
{
|
|
1320
|
-
name: "block-env-files",
|
|
1321
|
-
description: "Block reading/writing .env files",
|
|
1322
|
-
fn: blockEnvFiles,
|
|
1323
|
-
match: { events: ["PreToolUse"] },
|
|
1324
|
-
defaultEnabled: true,
|
|
1325
|
-
category: "Environment",
|
|
1326
|
-
},
|
|
1327
|
-
{
|
|
1328
|
-
name: "block-read-outside-cwd",
|
|
1329
|
-
description: "Block file reads outside the session working directory",
|
|
1330
|
-
fn: blockReadOutsideCwd,
|
|
1331
|
-
match: { events: ["PreToolUse"], toolNames: ["Read", "Glob", "Grep", "Bash"] },
|
|
1332
|
-
defaultEnabled: false,
|
|
1333
|
-
category: "Environment",
|
|
1334
|
-
params: {
|
|
1335
|
-
allowPaths: {
|
|
1336
|
-
type: "string[]",
|
|
1337
|
-
description: "Absolute paths outside cwd that are allowed to be read",
|
|
1338
|
-
default: [],
|
|
1339
|
-
},
|
|
1340
|
-
} satisfies PolicyParamsSchema,
|
|
1341
|
-
},
|
|
1342
|
-
{
|
|
1343
|
-
name: "block-sudo",
|
|
1344
|
-
description: "Block sudo commands",
|
|
1345
|
-
fn: blockSudo,
|
|
1346
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1347
|
-
defaultEnabled: true,
|
|
1348
|
-
category: "Dangerous Commands",
|
|
1349
|
-
params: {
|
|
1350
|
-
allowPatterns: {
|
|
1351
|
-
type: "string[]",
|
|
1352
|
-
description: "Sudo command patterns to allow, matched token-by-token (e.g. 'sudo systemctl status')",
|
|
1353
|
-
default: [],
|
|
1354
|
-
},
|
|
1355
|
-
} satisfies PolicyParamsSchema,
|
|
1356
|
-
},
|
|
1357
|
-
{
|
|
1358
|
-
name: "block-curl-pipe-sh",
|
|
1359
|
-
description: "Block piping downloads to shell",
|
|
1360
|
-
fn: blockCurlPipeSh,
|
|
1361
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1362
|
-
defaultEnabled: true,
|
|
1363
|
-
category: "Dangerous Commands",
|
|
1364
|
-
},
|
|
1365
|
-
{
|
|
1366
|
-
name: "block-rm-rf",
|
|
1367
|
-
description: "Prevent catastrophic deletions",
|
|
1368
|
-
fn: blockRmRf,
|
|
1369
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1370
|
-
defaultEnabled: false,
|
|
1371
|
-
category: "Dangerous Commands",
|
|
1372
|
-
params: {
|
|
1373
|
-
allowPaths: {
|
|
1374
|
-
type: "string[]",
|
|
1375
|
-
description: "Paths that are allowed to be recursively deleted",
|
|
1376
|
-
default: [],
|
|
1377
|
-
},
|
|
1378
|
-
} satisfies PolicyParamsSchema,
|
|
1379
|
-
},
|
|
1380
|
-
{
|
|
1381
|
-
name: "block-failproofai-commands",
|
|
1382
|
-
description: "Block failproofai CLI commands and uninstallation",
|
|
1383
|
-
fn: blockFailproofaiCommands,
|
|
1384
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1385
|
-
defaultEnabled: true,
|
|
1386
|
-
category: "Dangerous Commands",
|
|
1387
|
-
},
|
|
1388
|
-
{
|
|
1389
|
-
name: "block-secrets-write",
|
|
1390
|
-
description: "Block writing secret key files",
|
|
1391
|
-
fn: blockSecretsWrite,
|
|
1392
|
-
match: { events: ["PreToolUse"], toolNames: ["Write"] },
|
|
1393
|
-
defaultEnabled: false,
|
|
1394
|
-
category: "Dangerous Commands",
|
|
1395
|
-
params: {
|
|
1396
|
-
additionalPatterns: {
|
|
1397
|
-
type: "string[]",
|
|
1398
|
-
description: "Additional filename patterns (substrings) to block",
|
|
1399
|
-
default: [],
|
|
1400
|
-
},
|
|
1401
|
-
} satisfies PolicyParamsSchema,
|
|
1402
|
-
},
|
|
1403
|
-
{
|
|
1404
|
-
name: "block-push-master",
|
|
1405
|
-
description: "Block pushing to main/master",
|
|
1406
|
-
fn: blockPushMaster,
|
|
1407
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1408
|
-
defaultEnabled: true,
|
|
1409
|
-
category: "Git",
|
|
1410
|
-
params: {
|
|
1411
|
-
protectedBranches: {
|
|
1412
|
-
type: "string[]",
|
|
1413
|
-
description: "Branch names to protect from direct pushes",
|
|
1414
|
-
default: ["main", "master"],
|
|
1415
|
-
},
|
|
1416
|
-
} satisfies PolicyParamsSchema,
|
|
1417
|
-
},
|
|
1418
|
-
{
|
|
1419
|
-
name: "block-force-push",
|
|
1420
|
-
description: "Prevent force-pushing to any branch",
|
|
1421
|
-
fn: blockForcePush,
|
|
1422
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1423
|
-
defaultEnabled: false,
|
|
1424
|
-
category: "Git",
|
|
1425
|
-
},
|
|
1426
|
-
{
|
|
1427
|
-
name: "block-work-on-main",
|
|
1428
|
-
description: "Block git commits and merges on main/master branch",
|
|
1429
|
-
fn: blockWorkOnMain,
|
|
1430
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1431
|
-
defaultEnabled: false,
|
|
1432
|
-
category: "Git",
|
|
1433
|
-
params: {
|
|
1434
|
-
protectedBranches: {
|
|
1435
|
-
type: "string[]",
|
|
1436
|
-
description: "Branch names where commits/merges are blocked",
|
|
1437
|
-
default: ["main", "master"],
|
|
1438
|
-
},
|
|
1439
|
-
} satisfies PolicyParamsSchema,
|
|
1440
|
-
},
|
|
1441
|
-
{
|
|
1442
|
-
name: "warn-git-amend",
|
|
1443
|
-
description: "Warns before amending git commits, which rewrites history",
|
|
1444
|
-
fn: warnGitAmend,
|
|
1445
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1446
|
-
defaultEnabled: false,
|
|
1447
|
-
category: "Git",
|
|
1448
|
-
},
|
|
1449
|
-
{
|
|
1450
|
-
name: "warn-git-stash-drop",
|
|
1451
|
-
description: "Warns before permanently deleting stashed changes",
|
|
1452
|
-
fn: warnGitStashDrop,
|
|
1453
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1454
|
-
defaultEnabled: false,
|
|
1455
|
-
category: "Git",
|
|
1456
|
-
},
|
|
1457
|
-
{
|
|
1458
|
-
name: "warn-all-files-staged",
|
|
1459
|
-
description: "Warns before staging all working tree files with git add -A / . / --all",
|
|
1460
|
-
fn: warnAllFilesStaged,
|
|
1461
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1462
|
-
defaultEnabled: false,
|
|
1463
|
-
category: "Git",
|
|
1464
|
-
},
|
|
1465
|
-
{
|
|
1466
|
-
name: "warn-destructive-sql",
|
|
1467
|
-
description: "Warn before executing destructive SQL (DROP/TRUNCATE/DELETE without WHERE) via database clients",
|
|
1468
|
-
fn: warnDestructiveSql,
|
|
1469
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1470
|
-
defaultEnabled: false,
|
|
1471
|
-
category: "Database",
|
|
1472
|
-
},
|
|
1473
|
-
{
|
|
1474
|
-
name: "warn-schema-alteration",
|
|
1475
|
-
description: "Warns before SQL schema changes (ALTER TABLE with column or rename operations)",
|
|
1476
|
-
fn: warnSchemaAlteration,
|
|
1477
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1478
|
-
defaultEnabled: false,
|
|
1479
|
-
category: "Database",
|
|
1480
|
-
},
|
|
1481
|
-
{
|
|
1482
|
-
name: "warn-package-publish",
|
|
1483
|
-
description: "Warn before publishing packages to public registries (npm, PyPI, crates.io, RubyGems, etc.)",
|
|
1484
|
-
fn: warnPackagePublish,
|
|
1485
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1486
|
-
defaultEnabled: false,
|
|
1487
|
-
category: "Packages & System",
|
|
1488
|
-
},
|
|
1489
|
-
{
|
|
1490
|
-
name: "warn-global-package-install",
|
|
1491
|
-
description: "Warns before installing packages globally (npm -g, cargo install, etc.)",
|
|
1492
|
-
fn: warnGlobalPackageInstall,
|
|
1493
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1494
|
-
defaultEnabled: false,
|
|
1495
|
-
category: "Packages & System",
|
|
1496
|
-
},
|
|
1497
|
-
{
|
|
1498
|
-
name: "prefer-package-manager",
|
|
1499
|
-
description: "Blocks non-preferred package managers and tells Claude to use an allowed one (e.g., uv instead of pip)",
|
|
1500
|
-
fn: preferPackageManager,
|
|
1501
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1502
|
-
defaultEnabled: false,
|
|
1503
|
-
category: "Packages & System",
|
|
1504
|
-
params: {
|
|
1505
|
-
allowed: {
|
|
1506
|
-
type: "string[]",
|
|
1507
|
-
description: "Allowed package manager names (e.g. ['uv', 'bun']). Any detected manager not in this list is blocked.",
|
|
1508
|
-
default: [],
|
|
1509
|
-
},
|
|
1510
|
-
blocked: {
|
|
1511
|
-
type: "string[]",
|
|
1512
|
-
description: "Additional manager names to block beyond the built-in list (e.g. ['pdm', 'pipx']).",
|
|
1513
|
-
default: [],
|
|
1514
|
-
},
|
|
1515
|
-
} satisfies PolicyParamsSchema,
|
|
1516
|
-
},
|
|
1517
|
-
{
|
|
1518
|
-
name: "warn-large-file-write",
|
|
1519
|
-
description: "Warn before writing files larger than 1MB (configurable via thresholdKb param)",
|
|
1520
|
-
fn: warnLargeFileWrite,
|
|
1521
|
-
match: { events: ["PreToolUse"], toolNames: ["Write"] },
|
|
1522
|
-
defaultEnabled: false,
|
|
1523
|
-
category: "Packages & System",
|
|
1524
|
-
params: {
|
|
1525
|
-
thresholdKb: {
|
|
1526
|
-
type: "number",
|
|
1527
|
-
description: "File size threshold in KB above which a warning is issued",
|
|
1528
|
-
default: 1024,
|
|
1529
|
-
},
|
|
1530
|
-
} satisfies PolicyParamsSchema,
|
|
1531
|
-
},
|
|
1532
|
-
{
|
|
1533
|
-
name: "warn-background-process",
|
|
1534
|
-
description: "Warns before starting detached or background processes",
|
|
1535
|
-
fn: warnBackgroundProcess,
|
|
1536
|
-
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1537
|
-
defaultEnabled: false,
|
|
1538
|
-
category: "Packages & System",
|
|
1539
|
-
},
|
|
1540
|
-
{
|
|
1541
|
-
name: "warn-repeated-tool-calls",
|
|
1542
|
-
description: "Warn when the same tool is called 3+ times with identical parameters",
|
|
1543
|
-
fn: warnRepeatedToolCalls,
|
|
1544
|
-
match: { events: ["PreToolUse"] },
|
|
1545
|
-
defaultEnabled: false,
|
|
1546
|
-
category: "AI Behavior",
|
|
1547
|
-
},
|
|
1548
|
-
{
|
|
1549
|
-
name: "require-commit-before-stop",
|
|
1550
|
-
description: "Require all changes to be committed before Claude stops",
|
|
1551
|
-
fn: requireCommitBeforeStop,
|
|
1552
|
-
match: { events: ["Stop"] },
|
|
1553
|
-
defaultEnabled: false,
|
|
1554
|
-
category: "Workflow",
|
|
1555
|
-
},
|
|
1556
|
-
{
|
|
1557
|
-
name: "require-push-before-stop",
|
|
1558
|
-
description: "Require all commits to be pushed to remote before Claude stops",
|
|
1559
|
-
fn: requirePushBeforeStop,
|
|
1560
|
-
match: { events: ["Stop"] },
|
|
1561
|
-
defaultEnabled: false,
|
|
1562
|
-
category: "Workflow",
|
|
1563
|
-
params: {
|
|
1564
|
-
remote: {
|
|
1565
|
-
type: "string",
|
|
1566
|
-
description: "Remote name to push to (default: origin)",
|
|
1567
|
-
default: "origin",
|
|
1568
|
-
},
|
|
1569
|
-
baseBranch: {
|
|
1570
|
-
type: "string",
|
|
1571
|
-
description: "Base branch to compare against (default: main)",
|
|
1572
|
-
default: "main",
|
|
1573
|
-
},
|
|
1574
|
-
} satisfies PolicyParamsSchema,
|
|
1575
|
-
},
|
|
1576
|
-
{
|
|
1577
|
-
name: "require-pr-before-stop",
|
|
1578
|
-
description: "Require a pull request to exist for the current branch before Claude stops",
|
|
1579
|
-
fn: requirePrBeforeStop,
|
|
1580
|
-
match: { events: ["Stop"] },
|
|
1581
|
-
defaultEnabled: false,
|
|
1582
|
-
category: "Workflow",
|
|
1583
|
-
params: {
|
|
1584
|
-
baseBranch: {
|
|
1585
|
-
type: "string",
|
|
1586
|
-
description: "Base branch to compare against (default: main)",
|
|
1587
|
-
default: "main",
|
|
1588
|
-
},
|
|
1589
|
-
} satisfies PolicyParamsSchema,
|
|
1590
|
-
},
|
|
1591
|
-
{
|
|
1592
|
-
name: "require-ci-green-before-stop",
|
|
1593
|
-
description: "Require CI checks to pass on the current branch before Claude stops",
|
|
1594
|
-
fn: requireCiGreenBeforeStop,
|
|
1595
|
-
match: { events: ["Stop"] },
|
|
1596
|
-
defaultEnabled: false,
|
|
1597
|
-
category: "Workflow",
|
|
1598
|
-
},
|
|
1599
|
-
];
|
|
1600
|
-
|
|
1601
|
-
export function registerBuiltinPolicies(enabledNames: string[]): void {
|
|
1602
|
-
const enabledSet = new Set(enabledNames);
|
|
1603
|
-
for (const policy of BUILTIN_POLICIES) {
|
|
1604
|
-
if (enabledSet.has(policy.name)) {
|
|
1605
|
-
registerPolicy(policy.name, policy.description, policy.fn, policy.match);
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
/** Clears the git branch cache. Exposed for test isolation only. */
|
|
1611
|
-
export function clearGitBranchCache(): void {
|
|
1612
|
-
gitBranchCache.clear();
|
|
1613
|
-
}
|