cc-safe-setup 2.9.0 → 3.0.0
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/action.yml +29 -3
- package/index.mjs +262 -0
- package/package.json +2 -2
package/action.yml
CHANGED
|
@@ -10,21 +10,47 @@ inputs:
|
|
|
10
10
|
required: false
|
|
11
11
|
default: '70'
|
|
12
12
|
|
|
13
|
+
outputs:
|
|
14
|
+
score:
|
|
15
|
+
description: 'Safety score (0-100)'
|
|
16
|
+
value: ${{ steps.audit.outputs.score }}
|
|
17
|
+
grade:
|
|
18
|
+
description: 'Safety grade (A/B/C/F)'
|
|
19
|
+
value: ${{ steps.audit.outputs.grade }}
|
|
20
|
+
risks:
|
|
21
|
+
description: 'Number of risks found'
|
|
22
|
+
value: ${{ steps.audit.outputs.risks }}
|
|
23
|
+
|
|
13
24
|
runs:
|
|
14
25
|
using: 'composite'
|
|
15
26
|
steps:
|
|
16
27
|
- name: Run safety audit
|
|
28
|
+
id: audit
|
|
17
29
|
shell: bash
|
|
18
30
|
run: |
|
|
19
31
|
echo "::group::Claude Code Safety Audit"
|
|
32
|
+
npx cc-safe-setup@latest --audit --json 2>/dev/null > /tmp/audit-json.txt || true
|
|
20
33
|
npx cc-safe-setup@latest --audit 2>&1 | tee /tmp/audit-output.txt
|
|
21
34
|
echo "::endgroup::"
|
|
22
35
|
|
|
23
|
-
#
|
|
24
|
-
|
|
36
|
+
# Parse JSON output
|
|
37
|
+
if [ -s /tmp/audit-json.txt ]; then
|
|
38
|
+
SCORE=$(python3 -c "import json; print(json.load(open('/tmp/audit-json.txt'))['score'])" 2>/dev/null || echo "0")
|
|
39
|
+
GRADE=$(python3 -c "import json; print(json.load(open('/tmp/audit-json.txt'))['grade'])" 2>/dev/null || echo "?")
|
|
40
|
+
RISK_COUNT=$(python3 -c "import json; print(len(json.load(open('/tmp/audit-json.txt'))['risks']))" 2>/dev/null || echo "0")
|
|
41
|
+
else
|
|
42
|
+
SCORE=$(grep -oP 'Safety Score: \K\d+' /tmp/audit-output.txt || echo "0")
|
|
43
|
+
GRADE="?"
|
|
44
|
+
RISK_COUNT="?"
|
|
45
|
+
fi
|
|
46
|
+
|
|
25
47
|
THRESHOLD="${{ inputs.threshold }}"
|
|
26
48
|
|
|
27
|
-
echo "
|
|
49
|
+
echo "score=$SCORE" >> $GITHUB_OUTPUT
|
|
50
|
+
echo "grade=$GRADE" >> $GITHUB_OUTPUT
|
|
51
|
+
echo "risks=$RISK_COUNT" >> $GITHUB_OUTPUT
|
|
52
|
+
|
|
53
|
+
echo "Safety Score: $SCORE/100 (Grade: $GRADE, Risks: $RISK_COUNT, Threshold: $THRESHOLD)"
|
|
28
54
|
|
|
29
55
|
if [ "$SCORE" -lt "$THRESHOLD" ]; then
|
|
30
56
|
echo "::error::Safety score $SCORE is below threshold $THRESHOLD. Run 'npx cc-safe-setup --audit --fix' to improve."
|
package/index.mjs
CHANGED
|
@@ -78,6 +78,9 @@ const EXPORT = process.argv.includes('--export');
|
|
|
78
78
|
const IMPORT_IDX = process.argv.findIndex(a => a === '--import');
|
|
79
79
|
const IMPORT_FILE = IMPORT_IDX !== -1 ? process.argv[IMPORT_IDX + 1] : null;
|
|
80
80
|
const STATS = process.argv.includes('--stats');
|
|
81
|
+
const JSON_OUTPUT = process.argv.includes('--json');
|
|
82
|
+
const CREATE_IDX = process.argv.findIndex(a => a === '--create');
|
|
83
|
+
const CREATE_DESC = CREATE_IDX !== -1 ? process.argv.slice(CREATE_IDX + 1).join(' ') : null;
|
|
81
84
|
|
|
82
85
|
if (HELP) {
|
|
83
86
|
console.log(`
|
|
@@ -94,10 +97,12 @@ if (HELP) {
|
|
|
94
97
|
npx cc-safe-setup --full Complete setup: hooks + scan + audit + badge
|
|
95
98
|
npx cc-safe-setup --audit Safety score (0-100) with fixes
|
|
96
99
|
npx cc-safe-setup --audit --fix Auto-fix missing protections
|
|
100
|
+
npx cc-safe-setup --audit --json Machine-readable output for CI/CD
|
|
97
101
|
npx cc-safe-setup --scan Detect tech stack, recommend hooks
|
|
98
102
|
npx cc-safe-setup --learn Learn from your block history
|
|
99
103
|
npx cc-safe-setup --doctor Diagnose why hooks aren't working
|
|
100
104
|
npx cc-safe-setup --watch Live dashboard of blocked commands
|
|
105
|
+
npx cc-safe-setup --create "<desc>" Generate a custom hook from description
|
|
101
106
|
npx cc-safe-setup --stats Block statistics and patterns report
|
|
102
107
|
npx cc-safe-setup --export Export hooks config for team sharing
|
|
103
108
|
npx cc-safe-setup --import <file> Import hooks from exported config
|
|
@@ -596,7 +601,20 @@ async function audit() {
|
|
|
596
601
|
console.log(c.dim + ' Paste this into your README.md' + c.reset);
|
|
597
602
|
}
|
|
598
603
|
|
|
604
|
+
// JSON output (for CI/CD integration)
|
|
605
|
+
if (JSON_OUTPUT) {
|
|
606
|
+
const output = {
|
|
607
|
+
score,
|
|
608
|
+
grade: score >= 80 ? 'A' : score >= 60 ? 'B' : score >= 40 ? 'C' : 'F',
|
|
609
|
+
risks: risks.map(r => ({ severity: r.severity, issue: r.issue, fix: r.fix })),
|
|
610
|
+
passing: good,
|
|
611
|
+
timestamp: new Date().toISOString(),
|
|
612
|
+
};
|
|
613
|
+
console.log(JSON.stringify(output, null, 2));
|
|
614
|
+
}
|
|
615
|
+
|
|
599
616
|
console.log();
|
|
617
|
+
process.exit(score < (parseInt(process.env.CC_AUDIT_THRESHOLD) || 0) ? 1 : 0);
|
|
600
618
|
}
|
|
601
619
|
|
|
602
620
|
function learn() {
|
|
@@ -749,6 +767,249 @@ async function fullSetup() {
|
|
|
749
767
|
console.log();
|
|
750
768
|
}
|
|
751
769
|
|
|
770
|
+
async function createHook(description) {
|
|
771
|
+
console.log();
|
|
772
|
+
console.log(c.bold + ' cc-safe-setup --create' + c.reset);
|
|
773
|
+
console.log(c.dim + ' Generating hook from: "' + description + '"' + c.reset);
|
|
774
|
+
console.log();
|
|
775
|
+
|
|
776
|
+
const desc = description.toLowerCase();
|
|
777
|
+
|
|
778
|
+
// Pattern matching engine — matches description to hook templates
|
|
779
|
+
const patterns = [
|
|
780
|
+
{
|
|
781
|
+
match: /block.*(npm\s+publish|yarn\s+publish|pnpm\s+publish)/,
|
|
782
|
+
name: 'block-publish-without-tests',
|
|
783
|
+
trigger: 'PreToolUse', matcher: 'Bash',
|
|
784
|
+
script: `#!/bin/bash
|
|
785
|
+
INPUT=$(cat)
|
|
786
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
787
|
+
[ -z "$COMMAND" ] && exit 0
|
|
788
|
+
if echo "$COMMAND" | grep -qE '(npm|yarn|pnpm)\\s+publish'; then
|
|
789
|
+
# Check if tests were run recently
|
|
790
|
+
if [ -f "package.json" ]; then
|
|
791
|
+
TEST_CMD=$(python3 -c "import json; print(json.load(open('package.json')).get('scripts',{}).get('test',''))" 2>/dev/null)
|
|
792
|
+
if [ -n "$TEST_CMD" ]; then
|
|
793
|
+
echo "BLOCKED: Run tests before publishing." >&2
|
|
794
|
+
echo "Command: $COMMAND" >&2
|
|
795
|
+
echo "Run: npm test && npm publish" >&2
|
|
796
|
+
exit 2
|
|
797
|
+
fi
|
|
798
|
+
fi
|
|
799
|
+
fi
|
|
800
|
+
exit 0`,
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
match: /block.*(docker\s+rm|docker\s+system\s+prune|docker.*(?:remove|delete|prune))/,
|
|
804
|
+
name: 'block-docker-destructive',
|
|
805
|
+
trigger: 'PreToolUse', matcher: 'Bash',
|
|
806
|
+
script: `#!/bin/bash
|
|
807
|
+
INPUT=$(cat)
|
|
808
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
809
|
+
[ -z "$COMMAND" ] && exit 0
|
|
810
|
+
if echo "$COMMAND" | grep -qiE 'docker\\s+(system\\s+prune|rm\\s+-f|rmi\\s+-f|volume\\s+rm|network\\s+rm)'; then
|
|
811
|
+
echo "BLOCKED: Destructive docker command." >&2
|
|
812
|
+
echo "Command: $COMMAND" >&2
|
|
813
|
+
exit 2
|
|
814
|
+
fi
|
|
815
|
+
exit 0`,
|
|
816
|
+
},
|
|
817
|
+
{
|
|
818
|
+
match: /block.*(curl.*pipe|curl.*\|.*sh|wget.*pipe|wget.*\|)/,
|
|
819
|
+
name: 'block-curl-pipe-sh',
|
|
820
|
+
trigger: 'PreToolUse', matcher: 'Bash',
|
|
821
|
+
script: `#!/bin/bash
|
|
822
|
+
INPUT=$(cat)
|
|
823
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
824
|
+
[ -z "$COMMAND" ] && exit 0
|
|
825
|
+
if echo "$COMMAND" | grep -qE '(curl|wget)\\s.*\\|\\s*(bash|sh|zsh|python)'; then
|
|
826
|
+
echo "BLOCKED: Piping remote content to shell." >&2
|
|
827
|
+
echo "Command: $COMMAND" >&2
|
|
828
|
+
echo "Download first, review, then execute." >&2
|
|
829
|
+
exit 2
|
|
830
|
+
fi
|
|
831
|
+
exit 0`,
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
match: /block.*(pip\s+install|pip3\s+install).*(?:sudo|system|global)/,
|
|
835
|
+
name: 'block-global-pip',
|
|
836
|
+
trigger: 'PreToolUse', matcher: 'Bash',
|
|
837
|
+
script: `#!/bin/bash
|
|
838
|
+
INPUT=$(cat)
|
|
839
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
840
|
+
[ -z "$COMMAND" ] && exit 0
|
|
841
|
+
if echo "$COMMAND" | grep -qE '(sudo\\s+)?(pip3?|python3?\\s+-m\\s+pip)\\s+install' && ! echo "$COMMAND" | grep -qE '(--user|venv|virtualenv|-e\\s+\\.)'; then
|
|
842
|
+
echo "BLOCKED: pip install without --user or virtual environment." >&2
|
|
843
|
+
echo "Command: $COMMAND" >&2
|
|
844
|
+
echo "Use: pip install --user, or activate a virtualenv first." >&2
|
|
845
|
+
exit 2
|
|
846
|
+
fi
|
|
847
|
+
exit 0`,
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
match: /block.*(large\s+file|big\s+file|file\s+size|over\s+\d+)/,
|
|
851
|
+
name: 'block-large-writes',
|
|
852
|
+
trigger: 'PreToolUse', matcher: 'Write',
|
|
853
|
+
script: `#!/bin/bash
|
|
854
|
+
INPUT=$(cat)
|
|
855
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
856
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty' 2>/dev/null)
|
|
857
|
+
[ -z "$FILE" ] && exit 0
|
|
858
|
+
SIZE=\${#CONTENT}
|
|
859
|
+
LIMIT=\${CC_MAX_WRITE_SIZE:-500000}
|
|
860
|
+
if [ "$SIZE" -gt "$LIMIT" ]; then
|
|
861
|
+
echo "WARNING: Writing $SIZE bytes to $FILE (limit: $LIMIT)." >&2
|
|
862
|
+
echo "Set CC_MAX_WRITE_SIZE to adjust the limit." >&2
|
|
863
|
+
exit 2
|
|
864
|
+
fi
|
|
865
|
+
exit 0`,
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
match: /block.*(drop\s+table|truncate|delete\s+from|alter\s+table)/,
|
|
869
|
+
name: 'block-raw-sql-destructive',
|
|
870
|
+
trigger: 'PreToolUse', matcher: 'Bash',
|
|
871
|
+
script: `#!/bin/bash
|
|
872
|
+
INPUT=$(cat)
|
|
873
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
874
|
+
[ -z "$COMMAND" ] && exit 0
|
|
875
|
+
if echo "$COMMAND" | grep -qiE '(DROP\\s+TABLE|TRUNCATE\\s+TABLE|DELETE\\s+FROM\\s+[a-z]|ALTER\\s+TABLE.*DROP)'; then
|
|
876
|
+
echo "BLOCKED: Destructive SQL command detected." >&2
|
|
877
|
+
echo "Command: $COMMAND" >&2
|
|
878
|
+
exit 2
|
|
879
|
+
fi
|
|
880
|
+
exit 0`,
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
match: /auto.?approve.*(test|jest|pytest|mocha|vitest)/,
|
|
884
|
+
name: 'auto-approve-tests',
|
|
885
|
+
trigger: 'PreToolUse', matcher: 'Bash',
|
|
886
|
+
script: `#!/bin/bash
|
|
887
|
+
INPUT=$(cat)
|
|
888
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
889
|
+
[ -z "$COMMAND" ] && exit 0
|
|
890
|
+
if echo "$COMMAND" | grep -qE '^\\s*(npm\\s+test|npx\\s+(jest|vitest|mocha)|pytest|python3?\\s+-m\\s+pytest|cargo\\s+test|go\\s+test)'; then
|
|
891
|
+
jq -n '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"test command auto-approved"}}'
|
|
892
|
+
fi
|
|
893
|
+
exit 0`,
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
match: /warn.*(todo|fixme|hack|xxx)/i,
|
|
897
|
+
name: 'warn-todo-markers',
|
|
898
|
+
trigger: 'PostToolUse', matcher: 'Edit|Write',
|
|
899
|
+
script: `#!/bin/bash
|
|
900
|
+
INPUT=$(cat)
|
|
901
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
902
|
+
[ -z "$FILE" ] || [ ! -f "$FILE" ] && exit 0
|
|
903
|
+
COUNT=$(grep -ciE '(TODO|FIXME|HACK|XXX)' "$FILE" 2>/dev/null || echo 0)
|
|
904
|
+
[ "$COUNT" -gt 0 ] && echo "NOTE: $FILE has $COUNT TODO/FIXME markers." >&2
|
|
905
|
+
exit 0`,
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
match: /block.*(commit|push).*without.*(test|lint|check)/,
|
|
909
|
+
name: 'block-commit-without-checks',
|
|
910
|
+
trigger: 'PreToolUse', matcher: 'Bash',
|
|
911
|
+
script: `#!/bin/bash
|
|
912
|
+
INPUT=$(cat)
|
|
913
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
914
|
+
[ -z "$COMMAND" ] && exit 0
|
|
915
|
+
if echo "$COMMAND" | grep -qE 'git\\s+(commit|push)' && ! echo "$COMMAND" | grep -qE '(--no-verify|--allow-empty)'; then
|
|
916
|
+
if [ -f "package.json" ] && command -v npm &>/dev/null; then
|
|
917
|
+
npm test --silent 2>/dev/null
|
|
918
|
+
if [ $? -ne 0 ]; then
|
|
919
|
+
echo "BLOCKED: Tests failing. Fix tests before commit/push." >&2
|
|
920
|
+
exit 2
|
|
921
|
+
fi
|
|
922
|
+
fi
|
|
923
|
+
fi
|
|
924
|
+
exit 0`,
|
|
925
|
+
},
|
|
926
|
+
];
|
|
927
|
+
|
|
928
|
+
// Find matching pattern
|
|
929
|
+
let matched = null;
|
|
930
|
+
for (const p of patterns) {
|
|
931
|
+
if (p.match.test(desc)) {
|
|
932
|
+
matched = p;
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (!matched) {
|
|
938
|
+
// Generate a generic blocking hook from the description
|
|
939
|
+
const keywords = desc.match(/block\s+(.+)/i)?.[1] || desc;
|
|
940
|
+
const sanitized = keywords.replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').slice(0, 30);
|
|
941
|
+
matched = {
|
|
942
|
+
name: 'custom-' + sanitized,
|
|
943
|
+
trigger: 'PreToolUse', matcher: 'Bash',
|
|
944
|
+
script: `#!/bin/bash
|
|
945
|
+
# Custom hook: ${description}
|
|
946
|
+
# Generated by cc-safe-setup --create
|
|
947
|
+
# Edit the grep pattern to match your specific commands
|
|
948
|
+
INPUT=$(cat)
|
|
949
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
950
|
+
[ -z "$COMMAND" ] && exit 0
|
|
951
|
+
# TODO: Replace this pattern with the commands you want to block
|
|
952
|
+
if echo "$COMMAND" | grep -qiE '${keywords.replace(/'/g, '').replace(/\s+/g, '.*')}'; then
|
|
953
|
+
echo "BLOCKED: ${description}" >&2
|
|
954
|
+
echo "Command: $COMMAND" >&2
|
|
955
|
+
exit 2
|
|
956
|
+
fi
|
|
957
|
+
exit 0`,
|
|
958
|
+
};
|
|
959
|
+
console.log(c.yellow + ' No exact template match. Generated a generic hook.' + c.reset);
|
|
960
|
+
console.log(c.dim + ' Edit the grep pattern in the generated script.' + c.reset);
|
|
961
|
+
console.log();
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Write the hook script
|
|
965
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
966
|
+
const hookPath = join(HOOKS_DIR, matched.name + '.sh');
|
|
967
|
+
writeFileSync(hookPath, matched.script);
|
|
968
|
+
chmodSync(hookPath, 0o755);
|
|
969
|
+
|
|
970
|
+
console.log(c.green + ' ✓ Created: ' + hookPath + c.reset);
|
|
971
|
+
console.log(c.dim + ' Trigger: ' + matched.trigger + ', Matcher: ' + matched.matcher + c.reset);
|
|
972
|
+
|
|
973
|
+
// Register in settings.json
|
|
974
|
+
let settings = {};
|
|
975
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
976
|
+
try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
|
|
977
|
+
}
|
|
978
|
+
if (!settings.hooks) settings.hooks = {};
|
|
979
|
+
if (!settings.hooks[matched.trigger]) settings.hooks[matched.trigger] = [];
|
|
980
|
+
|
|
981
|
+
// Check if already registered
|
|
982
|
+
const existing = settings.hooks[matched.trigger].flatMap(e => (e.hooks || []).map(h => h.command));
|
|
983
|
+
if (!existing.some(cmd => cmd.includes(matched.name))) {
|
|
984
|
+
settings.hooks[matched.trigger].push({
|
|
985
|
+
matcher: matched.matcher,
|
|
986
|
+
hooks: [{ type: 'command', command: hookPath }],
|
|
987
|
+
});
|
|
988
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
989
|
+
console.log(c.green + ' ✓ Registered in settings.json' + c.reset);
|
|
990
|
+
} else {
|
|
991
|
+
console.log(c.dim + ' Already registered in settings.json' + c.reset);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// Quick test
|
|
995
|
+
const { spawnSync } = await import('child_process');
|
|
996
|
+
const testResult = spawnSync('bash', [hookPath], {
|
|
997
|
+
input: '{}',
|
|
998
|
+
timeout: 5000,
|
|
999
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1000
|
+
});
|
|
1001
|
+
if (testResult.status === 0) {
|
|
1002
|
+
console.log(c.green + ' ✓ Hook passes empty input test' + c.reset);
|
|
1003
|
+
} else {
|
|
1004
|
+
console.log(c.yellow + ' ! Hook exits ' + testResult.status + ' on empty input (may need adjustment)' + c.reset);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
console.log();
|
|
1008
|
+
console.log(c.dim + ' Test it: npx cc-hook-test ' + hookPath + c.reset);
|
|
1009
|
+
console.log(c.dim + ' Restart Claude Code to activate.' + c.reset);
|
|
1010
|
+
console.log();
|
|
1011
|
+
}
|
|
1012
|
+
|
|
752
1013
|
async function stats() {
|
|
753
1014
|
const LOG_PATH = join(HOME, '.claude', 'blocked-commands.log');
|
|
754
1015
|
|
|
@@ -1427,6 +1688,7 @@ async function main() {
|
|
|
1427
1688
|
if (FULL) return fullSetup();
|
|
1428
1689
|
if (DOCTOR) return doctor();
|
|
1429
1690
|
if (WATCH) return watch();
|
|
1691
|
+
if (CREATE_DESC) return createHook(CREATE_DESC);
|
|
1430
1692
|
if (STATS) return stats();
|
|
1431
1693
|
if (EXPORT) return exportConfig();
|
|
1432
1694
|
if (IMPORT_FILE) return importConfig(IMPORT_FILE);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "One command to make Claude Code safe for autonomous operation. 8 built-in hooks + 26 installable examples. Destructive blocker, branch guard, database wipe protection, case-insensitive FS guard, and more.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"bin": {
|
|
@@ -36,4 +36,4 @@
|
|
|
36
36
|
"url": "https://github.com/yurukusa/cc-safe-setup"
|
|
37
37
|
},
|
|
38
38
|
"homepage": "https://github.com/yurukusa/cc-safe-setup"
|
|
39
|
-
}
|
|
39
|
+
}
|