arkaos 2.63.0 → 2.65.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/VERSION +1 -1
- package/config/hooks/stop.sh +17 -0
- package/config/hooks/user-prompt-submit.sh +17 -2
- package/core/governance/__pycache__/skill_proposer.cpython-313.pyc +0 -0
- package/installer/hard-deny.js +163 -0
- package/installer/index.js +19 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.65.0
|
package/config/hooks/stop.sh
CHANGED
|
@@ -19,12 +19,25 @@ SESSION_ID=""
|
|
|
19
19
|
TRANSCRIPT_PATH=""
|
|
20
20
|
STOP_HOOK_ACTIVE=""
|
|
21
21
|
CWD=""
|
|
22
|
+
EFFORT_LEVEL=""
|
|
22
23
|
if command -v jq &>/dev/null; then
|
|
23
24
|
SESSION_ID=$(echo "$input" | jq -r '.session_id // ""' 2>/dev/null)
|
|
24
25
|
TRANSCRIPT_PATH=$(echo "$input" | jq -r '.transcript_path // ""' 2>/dev/null)
|
|
25
26
|
STOP_HOOK_ACTIVE=$(echo "$input" | jq -r '.stop_hook_active // ""' 2>/dev/null)
|
|
26
27
|
CWD=$(echo "$input" | jq -r '.cwd // ""' 2>/dev/null)
|
|
28
|
+
# PR46 v2.65.0 — Claude Code W19 ships effort.level in hook stdin and
|
|
29
|
+
# $CLAUDE_EFFORT env var. Soft-block checks (kb-cite, meta-tag) only
|
|
30
|
+
# run at high|xhigh; hard enforcement runs regardless.
|
|
31
|
+
EFFORT_LEVEL=$(echo "$input" | jq -r '.effort.level // ""' 2>/dev/null)
|
|
27
32
|
fi
|
|
33
|
+
# Fallback to env var if stdin didn't carry it
|
|
34
|
+
[ -z "$EFFORT_LEVEL" ] && EFFORT_LEVEL="${CLAUDE_EFFORT:-}"
|
|
35
|
+
|
|
36
|
+
# Telemetry-only signal. Soft-block checks (kb_cite, meta_tag, sycophancy)
|
|
37
|
+
# always run here because they're cheap and feed /arka compliance.
|
|
38
|
+
# What is effort-gated is the NUDGE SURFACING in user-prompt-submit.sh
|
|
39
|
+
# (whether the next turn sees a [arka:suggest] line). Record the level
|
|
40
|
+
# on the telemetry row so we can later analyze suppression rates.
|
|
28
41
|
|
|
29
42
|
# Prevent infinite loops when Stop hook was triggered by its own decision.
|
|
30
43
|
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
|
|
@@ -60,6 +73,7 @@ SESSION_ID_VAL="$SESSION_ID" \
|
|
|
60
73
|
TRANSCRIPT_PATH_VAL="$TRANSCRIPT_PATH" \
|
|
61
74
|
CWD_VAL="$CWD" \
|
|
62
75
|
ARKAOS_ROOT_VAL="$ARKAOS_ROOT" \
|
|
76
|
+
EFFORT_LEVEL_VAL="$EFFORT_LEVEL" \
|
|
63
77
|
python3 - <<'PY' 2>/dev/null
|
|
64
78
|
import json
|
|
65
79
|
import os
|
|
@@ -223,6 +237,9 @@ entry = {
|
|
|
223
237
|
"kb_cite_topic_score": cite_topic_score,
|
|
224
238
|
"meta_tag_check_passed": meta_passed,
|
|
225
239
|
"meta_tag_check_reason": meta_reason,
|
|
240
|
+
# PR46 v2.65.0 — Claude Code effort level captured for later analysis
|
|
241
|
+
# of nudge-suppression rates. Unset / unknown values land as "".
|
|
242
|
+
"effort_level": os.environ.get("EFFORT_LEVEL_VAL", ""),
|
|
226
243
|
"mode": "warn",
|
|
227
244
|
}
|
|
228
245
|
|
|
@@ -84,10 +84,25 @@ mkdir -p "$CACHE_DIR" 2>/dev/null
|
|
|
84
84
|
# ─── Extract user input from hook JSON ───────────────────────────────────
|
|
85
85
|
user_input=""
|
|
86
86
|
SESSION_ID=""
|
|
87
|
+
EFFORT_LEVEL=""
|
|
87
88
|
if command -v jq &>/dev/null; then
|
|
88
89
|
user_input=$(echo "$input" | jq -r '.userInput // .message // ""' 2>/dev/null)
|
|
89
90
|
SESSION_ID=$(echo "$input" | jq -r '.session_id // ""' 2>/dev/null)
|
|
91
|
+
# PR46 v2.65.0 — Claude Code W19 ships effort.level in hook stdin.
|
|
92
|
+
# Soft-block nudges (KB-first + meta-tag) are gated by effort: only
|
|
93
|
+
# surfaced at high|xhigh; low/medium skip the nudge to avoid forcing
|
|
94
|
+
# the model to comply with full contracts during cheap exploratory
|
|
95
|
+
# turns. Hard enforcement (PreToolUse flow_enforcer) runs regardless.
|
|
96
|
+
EFFORT_LEVEL=$(echo "$input" | jq -r '.effort.level // ""' 2>/dev/null)
|
|
90
97
|
fi
|
|
98
|
+
[ -z "$EFFORT_LEVEL" ] && EFFORT_LEVEL="${CLAUDE_EFFORT:-}"
|
|
99
|
+
|
|
100
|
+
# Decide whether soft-block nudges surface to the next turn.
|
|
101
|
+
_ARKA_SURFACE_NUDGES="true"
|
|
102
|
+
case "${EFFORT_LEVEL:-high}" in
|
|
103
|
+
low|medium) _ARKA_SURFACE_NUDGES="false" ;;
|
|
104
|
+
*) _ARKA_SURFACE_NUDGES="true" ;;
|
|
105
|
+
esac
|
|
91
106
|
|
|
92
107
|
# ─── Flow marker cache invalidation (v2 — new turn, reset ALLOW cache) ──
|
|
93
108
|
# Cheap, non-blocking, runs before Synapse so a stuck Python later cannot
|
|
@@ -376,7 +391,7 @@ fi
|
|
|
376
391
|
# the suggestion to the model in this turn's additionalContext. One-shot:
|
|
377
392
|
# the file is deleted after read so the nudge does not repeat across turns.
|
|
378
393
|
_KB_CITE_NUDGE=""
|
|
379
|
-
if [ -n "$SESSION_ID" ]; then
|
|
394
|
+
if [ -n "$SESSION_ID" ] && [ "$_ARKA_SURFACE_NUDGES" = "true" ]; then
|
|
380
395
|
_CITE_FILE="/tmp/arkaos-cite/${SESSION_ID}.json"
|
|
381
396
|
if [ -f "$_CITE_FILE" ]; then
|
|
382
397
|
if command -v jq &>/dev/null; then
|
|
@@ -397,7 +412,7 @@ fi
|
|
|
397
412
|
# Mirror of the KB citation nudge but for the [arka:meta] one-liner
|
|
398
413
|
# contract. One-shot; deleted after read.
|
|
399
414
|
_META_TAG_NUDGE=""
|
|
400
|
-
if [ -n "$SESSION_ID" ]; then
|
|
415
|
+
if [ -n "$SESSION_ID" ] && [ "$_ARKA_SURFACE_NUDGES" = "true" ]; then
|
|
401
416
|
_META_FILE="/tmp/arkaos-meta/${SESSION_ID}.json"
|
|
402
417
|
if [ -f "$_META_FILE" ]; then
|
|
403
418
|
if command -v jq &>/dev/null; then
|
|
Binary file
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// autoMode.hard_deny defaults for Claude Code (PR45 v2.64.0).
|
|
2
|
+
//
|
|
3
|
+
// Claude Code v2.1.131+ shipped `autoMode.hard_deny` — actions that block
|
|
4
|
+
// unconditionally in auto mode regardless of allow rules. Without this,
|
|
5
|
+
// auto mode is structurally unsafe (allowlist alone cannot express
|
|
6
|
+
// "never run this even if a broader allow matches"). PR45 ships a
|
|
7
|
+
// curated default deny list and merges it into ~/.claude/settings.json
|
|
8
|
+
// without clobbering operator-authored entries.
|
|
9
|
+
//
|
|
10
|
+
// Operator extensions live in ~/.arkaos/hard-deny.json — installer
|
|
11
|
+
// reads it on every run and merges into the Claude settings file.
|
|
12
|
+
//
|
|
13
|
+
// Behaviour:
|
|
14
|
+
// - No-op when runtime is not Claude Code
|
|
15
|
+
// - Idempotent: merges by string equality; duplicates are dropped
|
|
16
|
+
// - Atomic write via .tmp + rename
|
|
17
|
+
// - Preserves all other settings.json content untouched
|
|
18
|
+
// - Never raises — failures logged but don't break the installer
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
existsSync, readFileSync, writeFileSync, renameSync, copyFileSync, mkdirSync,
|
|
22
|
+
} from "node:fs";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
import { join, dirname } from "node:path";
|
|
25
|
+
|
|
26
|
+
// Curated default deny list. Each entry follows Claude Code's
|
|
27
|
+
// permission-rule syntax: ToolName(pattern). The patterns are
|
|
28
|
+
// load-bearing — pick conservatively, since auto mode without these
|
|
29
|
+
// rules silently allows them when a broader allow matches.
|
|
30
|
+
export const DEFAULT_HARD_DENY_RULES = [
|
|
31
|
+
// Destructive git operations
|
|
32
|
+
"Bash(git push --force*)",
|
|
33
|
+
"Bash(git push -f*)",
|
|
34
|
+
"Bash(git reset --hard*)",
|
|
35
|
+
"Bash(git clean -fd*)",
|
|
36
|
+
"Bash(git branch -D*)",
|
|
37
|
+
|
|
38
|
+
// Filesystem destruction
|
|
39
|
+
"Bash(rm -rf /*)",
|
|
40
|
+
"Bash(rm -rf ~/*)",
|
|
41
|
+
"Bash(rm -rf ~)",
|
|
42
|
+
"Bash(rm -rf .*)",
|
|
43
|
+
|
|
44
|
+
// Secrets and credential paths
|
|
45
|
+
"Read(~/.ssh/*)",
|
|
46
|
+
"Read(~/.ssh/**)",
|
|
47
|
+
"Read(~/.aws/credentials)",
|
|
48
|
+
"Read(~/.aws/config)",
|
|
49
|
+
"Read(~/.gnupg/*)",
|
|
50
|
+
"Read(~/.gnupg/**)",
|
|
51
|
+
"Read(~/.npmrc)",
|
|
52
|
+
"Read(~/.docker/config.json)",
|
|
53
|
+
"Read(~/.config/gh/*)",
|
|
54
|
+
"Read(/etc/shadow)",
|
|
55
|
+
"Read(/etc/sudoers)",
|
|
56
|
+
|
|
57
|
+
// Writes to credential / config dirs
|
|
58
|
+
"Write(~/.ssh/*)",
|
|
59
|
+
"Write(~/.aws/credentials)",
|
|
60
|
+
"Write(~/.gnupg/*)",
|
|
61
|
+
"Write(~/.npmrc)",
|
|
62
|
+
|
|
63
|
+
// Process / privilege escalation
|
|
64
|
+
"Bash(sudo *)",
|
|
65
|
+
"Bash(su -*)",
|
|
66
|
+
"Bash(chmod 777*)",
|
|
67
|
+
"Bash(curl * | sh*)",
|
|
68
|
+
"Bash(curl * | bash*)",
|
|
69
|
+
"Bash(wget * | sh*)",
|
|
70
|
+
"Bash(wget * | bash*)",
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
// Operator extension file. Lines added here are merged into the
|
|
75
|
+
// Claude settings on every install/update.
|
|
76
|
+
const _USER_EXTENSION_FILE = "hard-deny.json";
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
export function seedAutoModeHardDeny({
|
|
80
|
+
runtime = "claude-code",
|
|
81
|
+
home = homedir(),
|
|
82
|
+
defaults = DEFAULT_HARD_DENY_RULES,
|
|
83
|
+
} = {}) {
|
|
84
|
+
if (runtime !== "claude-code") {
|
|
85
|
+
return { skipped: "runtime-not-claude-code", action: null, count: 0 };
|
|
86
|
+
}
|
|
87
|
+
const settingsPath = join(home, ".claude", "settings.json");
|
|
88
|
+
if (!existsSync(settingsPath)) {
|
|
89
|
+
return { skipped: "claude-settings-not-found", action: null, count: 0 };
|
|
90
|
+
}
|
|
91
|
+
const userExtensions = readUserExtensions(home);
|
|
92
|
+
const merged = mergeUnique(defaults, userExtensions);
|
|
93
|
+
return writeMergedSettings(settingsPath, merged);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
function readUserExtensions(home) {
|
|
98
|
+
const path = join(home, ".arkaos", _USER_EXTENSION_FILE);
|
|
99
|
+
if (!existsSync(path)) return [];
|
|
100
|
+
try {
|
|
101
|
+
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
102
|
+
const rules = Array.isArray(data.hard_deny) ? data.hard_deny : [];
|
|
103
|
+
return rules.filter((r) => typeof r === "string" && r.length > 0);
|
|
104
|
+
} catch {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
function mergeUnique(a, b) {
|
|
111
|
+
const seen = new Set();
|
|
112
|
+
const merged = [];
|
|
113
|
+
for (const rule of [...a, ...b]) {
|
|
114
|
+
if (!seen.has(rule)) {
|
|
115
|
+
seen.add(rule);
|
|
116
|
+
merged.push(rule);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return merged;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
function writeMergedSettings(settingsPath, mergedRules) {
|
|
124
|
+
let settings;
|
|
125
|
+
try {
|
|
126
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
127
|
+
} catch {
|
|
128
|
+
return { skipped: "settings-not-parseable", action: null, count: 0 };
|
|
129
|
+
}
|
|
130
|
+
if (typeof settings !== "object" || settings === null) {
|
|
131
|
+
return { skipped: "settings-not-object", action: null, count: 0 };
|
|
132
|
+
}
|
|
133
|
+
settings.autoMode = settings.autoMode && typeof settings.autoMode === "object"
|
|
134
|
+
? settings.autoMode
|
|
135
|
+
: {};
|
|
136
|
+
const existing = Array.isArray(settings.autoMode.hard_deny)
|
|
137
|
+
? settings.autoMode.hard_deny
|
|
138
|
+
: [];
|
|
139
|
+
// Preserve any operator-authored rules already in settings.json,
|
|
140
|
+
// then merge our defaults on top. Operator wins on duplicates
|
|
141
|
+
// because they appear first in mergeUnique.
|
|
142
|
+
const finalRules = mergeUnique(existing, mergedRules);
|
|
143
|
+
if (sameRules(existing, finalRules)) {
|
|
144
|
+
return { skipped: null, action: "noop", count: finalRules.length };
|
|
145
|
+
}
|
|
146
|
+
settings.autoMode.hard_deny = finalRules;
|
|
147
|
+
// Atomic write
|
|
148
|
+
const tmp = `${settingsPath}.tmp-${process.pid}`;
|
|
149
|
+
writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
|
|
150
|
+
renameSync(tmp, settingsPath);
|
|
151
|
+
return {
|
|
152
|
+
skipped: null,
|
|
153
|
+
action: existing.length === 0 ? "created" : "merged",
|
|
154
|
+
count: finalRules.length,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
function sameRules(a, b) {
|
|
160
|
+
if (a.length !== b.length) return false;
|
|
161
|
+
const setA = new Set(a);
|
|
162
|
+
return b.every((r) => setA.has(r));
|
|
163
|
+
}
|
package/installer/index.js
CHANGED
|
@@ -311,6 +311,25 @@ export async function install({ runtime, path, force, skipSystem, withOllama })
|
|
|
311
311
|
console.log(` Warning: could not scaffold user-data (${err.message})`);
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
+
// PR45 v2.64.0 — seed autoMode.hard_deny defaults into
|
|
315
|
+
// ~/.claude/settings.json so auto mode blocks destructive actions
|
|
316
|
+
// (git push --force, ~/.ssh reads, rm -rf, sudo, etc.) regardless
|
|
317
|
+
// of allow rules. Preserves operator-authored entries; merges
|
|
318
|
+
// operator extensions from ~/.arkaos/hard-deny.json.
|
|
319
|
+
try {
|
|
320
|
+
const { seedAutoModeHardDeny } = await import("./hard-deny.js");
|
|
321
|
+
const denyResult = seedAutoModeHardDeny({ runtime });
|
|
322
|
+
if (!denyResult.skipped) {
|
|
323
|
+
if (denyResult.action === "created") {
|
|
324
|
+
console.log(` autoMode.hard_deny created (${denyResult.count} rules).`);
|
|
325
|
+
} else if (denyResult.action === "merged") {
|
|
326
|
+
console.log(` autoMode.hard_deny merged (${denyResult.count} rules, operator entries preserved).`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} catch (err) {
|
|
330
|
+
console.log(` Warning: could not seed autoMode.hard_deny (${err.message})`);
|
|
331
|
+
}
|
|
332
|
+
|
|
314
333
|
// PR43 v2.62.0 — auto-install default Claude Code plugins. Only runs
|
|
315
334
|
// when runtime is Claude Code AND the `claude` CLI is available.
|
|
316
335
|
// Idempotent: skips plugins already in installed_plugins.json.
|
package/package.json
CHANGED