@ccoalm/ccl-skills 0.6.1 → 0.7.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/README.md +55 -15
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/hooks.json +11 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/remind-unverified-cli-flag.sh +309 -0
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_remind_unverified_cli_flag.sh +483 -0
- package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/ccl-skills.ts +5 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/SKILL.md +2 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/architecture-playbook.md +2 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/data-platform-architecture.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/event-driven-architecture.md +14 -11
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/multi-tenant-isolation.md +2 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/SKILL.md +1 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/SKILL.md +2 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/sli-slo-design.md +25 -9
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/source-register.md +1 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/promotion-gate-and-review.md +16 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/secret-and-config-management.md +7 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/retry-timeout-circuit-breaker.md +11 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/SKILL.md +5 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/architecture-playbook.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/audit-history-architecture.md +31 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/data-platform-architecture.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/event-driven-architecture.md +7 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/multi-tenant-isolation.md +2 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/notification-architecture.md +28 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/packaging-runtime-readiness.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/replay-comparison-architecture.md +28 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/workflow-state-architecture.md +39 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/SKILL.md +6 -6
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/ai-service-wiring-patterns.md +8 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/audit-history-patterns.md +29 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/background-job-patterns.md +16 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/batch-and-artifact-patterns.md +25 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/notification-patterns.md +40 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/public-api-security-patterns.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/replay-comparison-patterns.md +30 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/state-machine-task-patterns.md +48 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/testing-and-quality-patterns.md +10 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/SKILL.md +2 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/coverage-exhaustion-traps.md +45 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +48 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/external-practice-controls.md +21 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/firing-point-placement.md +8 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/parallel-stack-references-pattern.md +5 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +15 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +2 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +24 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-parallel-stack-parity.sh +119 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_parallel_stack_parity.sh +183 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +2 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/terminal-cli-dev/SKILL.md +1 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/SKILL.md +3 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/fitness-functions.md +16 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/scenario-testing.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/test-code-authoring-patterns.md +16 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/SKILL.md +5 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/delivery-face-closeout.md +16 -6
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/self-benchmark-baseline.md +37 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/SKILL.md +1 -0
- package/dist/assets/release.json +115 -50
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# @ccoalm/ccl-skills
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@ccoalm/ccl-skills)
|
|
4
|
+
[](https://www.npmjs.com/package/@ccoalm/ccl-skills)
|
|
5
|
+
[](https://github.com/ccoalm/ccl-skills/blob/main/LICENSE)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
**Reusable workflows that help coding agents plan, build, test, review, and release software.**
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
9
|
+
Not a prompt pack — a routed delivery system. 32 skills cover the whole lifecycle, and a routing layer reads what you asked for and hands it to the skill that owns that deliverable, so you never look one up.
|
|
10
|
+
|
|
11
|
+
Ask it to fix a bug and `defect-diagnosis` takes over: reproduce from first-hand failure evidence, isolate the cause, verify the fix, leave a regression test behind. Ask for a feature and `product-rd-workflow` routes it through requirement shaping, risk gates, implementation, and release. Ask it to touch code at all and `worktree-isolation` puts the work on its own branch first. Every skill also states when *not* to use it and which one to use instead — that is what keeps the routing sharp.
|
|
12
|
+
|
|
13
|
+
Each skill is a method, not a suggestion, and it ships with the gate that protects it. The methods are what worked, written down. The gates are what went wrong, turned into a stop: no patch before a reproduction, no edit on `main`, no *done* without evidence. A routing eval bank and CI gates check that both still fire.
|
|
10
14
|
|
|
11
15
|
## Install
|
|
12
16
|
|
|
@@ -15,24 +19,50 @@ npm install --global @ccoalm/ccl-skills
|
|
|
15
19
|
ccl-skills install
|
|
16
20
|
```
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
Restart your CLI so it reloads the skills.
|
|
23
|
+
|
|
24
|
+
`install` configures every host it detects. The package carries an immutable snapshot of the skills, agent context, plugin manifests, and runtime hooks, so installation needs no Git checkout.
|
|
25
|
+
|
|
26
|
+
Requirements: Node.js 20 or later, macOS or Linux, and at least one host CLI — Claude Code, Codex 0.133.0 or later, or OpenCode.
|
|
27
|
+
|
|
28
|
+
Run it without a global install:
|
|
19
29
|
|
|
20
30
|
```bash
|
|
21
31
|
npx @ccoalm/ccl-skills@latest install
|
|
22
32
|
```
|
|
23
33
|
|
|
24
|
-
|
|
34
|
+
## What you get
|
|
35
|
+
|
|
36
|
+
| Stage | Covered by the skills |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| Requirements | intent and acceptance points, current-state baseline, change scope and slicing, PRD writing |
|
|
39
|
+
| Design | layout, interaction, states, accessibility, design-system consistency |
|
|
40
|
+
| Architecture | Go and Python service boundaries, RPC and API contracts, data ownership, reliability invariants |
|
|
41
|
+
| Implementation | Go, Python, React web, mini-programs, Flutter/React Native/iOS/Android, terminal and TUI, LLM integration |
|
|
42
|
+
| Testing | test layers, fixtures, mocks, regression coverage, CI gates, test-case documents, defect diagnosis |
|
|
43
|
+
| Review | independent CLI review, adversarial challenge, risk and gate routing, multi-perspective research |
|
|
44
|
+
| Release | rollout, canary, rollback, environment lanes, release scope and docs |
|
|
45
|
+
| Operations | logs, metrics, tracing, dashboards, alerts, SLO, service connectivity |
|
|
46
|
+
| Cross-stage | worktree isolation, multi-agent delegation, doc tightening, lesson extraction |
|
|
47
|
+
|
|
48
|
+
[Browse the full catalog](https://github.com/ccoalm/ccl-skills/blob/main/docs/SKILLS.md) for what each skill does and when to use another one instead. [Architecture](https://github.com/ccoalm/ccl-skills/blob/main/docs/ARCHITECTURE.md) explains how skills are selected, loaded, and checked; [Theory](https://github.com/ccoalm/ccl-skills/blob/main/docs/skills-theory-foundations.md) explains why the rules read the way they do.
|
|
49
|
+
|
|
50
|
+
This page ships inside the published tarball, so it only changes when a new version is released. The links above always show the current state.
|
|
51
|
+
|
|
52
|
+
## Commands
|
|
25
53
|
|
|
26
54
|
```bash
|
|
27
|
-
ccl-skills install
|
|
28
|
-
ccl-skills doctor
|
|
29
|
-
ccl-skills update
|
|
30
|
-
ccl-skills update --yes
|
|
31
|
-
ccl-skills uninstall
|
|
32
|
-
ccl-skills uninstall --yes
|
|
55
|
+
ccl-skills install # write host assets
|
|
56
|
+
ccl-skills doctor # report install state and drift from the manifest
|
|
57
|
+
ccl-skills update # preview
|
|
58
|
+
ccl-skills update --yes # upgrade the package, then refresh host assets
|
|
59
|
+
ccl-skills uninstall # preview
|
|
60
|
+
ccl-skills uninstall --yes # remove host assets
|
|
33
61
|
```
|
|
34
62
|
|
|
35
|
-
|
|
63
|
+
Limit any operation to one host with `--host claude`, `--host codex`, or `--host opencode`. Add `--json` for machine-readable output.
|
|
64
|
+
|
|
65
|
+
`update` and `uninstall` are previews unless `--yes` is supplied. `update --yes` first upgrades the global npm package to `@latest`, then asks the freshly installed CLI to refresh host assets. Set `CCL_SKILLS_SKIP_SELF_UPDATE=1` for an assets-only refresh; `--allow-downgrade` always uses the currently invoked package without installing `@latest` first.
|
|
36
66
|
|
|
37
67
|
After `ccl-skills uninstall --yes`, remove the CLI package itself with `npm uninstall --global @ccoalm/ccl-skills` if it is no longer needed.
|
|
38
68
|
|
|
@@ -48,7 +78,15 @@ Silence: CCL_SKILLS_NO_UPDATE_NOTIFIER=1
|
|
|
48
78
|
|
|
49
79
|
The registry check runs at most once every 24 hours in a detached background process, so no invocation waits for the network, and its result is shown by the next run. The notice and its check are both silent under `--json`, in CI, when stdout or stderr is not a terminal, in terminals narrower than 60 columns, and when `CCL_SKILLS_NO_UPDATE_NOTIFIER` or `NO_UPDATE_NOTIFIER` is set. State lives in `version-check.json` under the managed root; a failed check keeps the last known version and never blocks or fails the command.
|
|
50
80
|
|
|
51
|
-
|
|
81
|
+
## What lands where
|
|
82
|
+
|
|
83
|
+
| Host | What the installer writes |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| Claude Code | A package-owned local marketplace snapshot; the plugin hooks are consumed directly. |
|
|
86
|
+
| Codex | The same package-owned local marketplace snapshot. |
|
|
87
|
+
| OpenCode | Skills, a native event plugin, and `ccl-skills/runtime` under shared host directories. The adapter maps the shipped session, tool, prompt, subagent, and stop behaviors to OpenCode events and covers `edit`, `write`, and `apply_patch`. |
|
|
88
|
+
|
|
89
|
+
Installation refuses unknown collisions, and uninstall preserves shared files for manual cleanup instead of guessing ownership.
|
|
52
90
|
|
|
53
91
|
Set `CCL_SKILLS_REPO` to a valid checkout to override only the OpenCode asset source. An invalid override fails before writing host files. Without the variable, installation is offline after npm has downloaded the package.
|
|
54
92
|
|
|
@@ -59,3 +97,5 @@ npm ci
|
|
|
59
97
|
npm test
|
|
60
98
|
npm run test:pack
|
|
61
99
|
```
|
|
100
|
+
|
|
101
|
+
Issues and pull requests go to [ccoalm/ccl-skills](https://github.com/ccoalm/ccl-skills); see [CONTRIBUTING](https://github.com/ccoalm/ccl-skills/blob/main/docs/CONTRIBUTING.md).
|
|
@@ -47,6 +47,17 @@
|
|
|
47
47
|
}
|
|
48
48
|
]
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
"matcher": "Bash",
|
|
52
|
+
"hooks": [
|
|
53
|
+
{
|
|
54
|
+
"type": "command",
|
|
55
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/remind-unverified-cli-flag.sh\"",
|
|
56
|
+
"timeout": 10,
|
|
57
|
+
"async": false
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
},
|
|
50
61
|
{
|
|
51
62
|
"matcher": "Task|Agent",
|
|
52
63
|
"hooks": [
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PreToolUse advisory — read the local CLI's own help before passing it a long
|
|
3
|
+
# flag (Bash tool only).
|
|
4
|
+
#
|
|
5
|
+
# WHY: distilled from 399 recorded agent failures across 122 sessions (round
|
|
6
|
+
# 059). "Flag guessed instead of read" accounts for ~20 of them, and the SAME
|
|
7
|
+
# flag — `glab mr list --state` — failed in EIGHT independent sessions spread
|
|
8
|
+
# over months. Every one of those eight was "fixed" the same way (read help, use
|
|
9
|
+
# the supported flag) and every fix only repaired that one instance, so the class
|
|
10
|
+
# came straight back. Same shape for `--jq` (3x), `--output`, `--pipeline-id`,
|
|
11
|
+
# `--branch`, `--old-text`, `--allow-scripts`.
|
|
12
|
+
#
|
|
13
|
+
# The rule already existed in prose, but only at two NARROW firing points:
|
|
14
|
+
# `worktree-isolation` (merge-authorization execution) and
|
|
15
|
+
# `skill-extraction-workflow` (verifying CLI examples written INTO a reference).
|
|
16
|
+
# Neither fires where the corpus actually fails: an agent about to run an
|
|
17
|
+
# ordinary inspection command. This hook is that missing firing point.
|
|
18
|
+
#
|
|
19
|
+
# DELIBERATELY DOES NOT PARSE THE COMMAND. An earlier version segmented on shell
|
|
20
|
+
# separators and handled heredocs, line continuations, the `--` terminator,
|
|
21
|
+
# launcher words, global options and hierarchical subcommands. Independent review
|
|
22
|
+
# found a new legal shell form it mishandled in essentially every round — fifteen
|
|
23
|
+
# of them — because the set of legal shell spellings is open-ended and a regex
|
|
24
|
+
# parser can only enumerate a prefix of it. This advisory does not need to know
|
|
25
|
+
# where the subcommand is or when a heredoc ends. It needs to know that a risky
|
|
26
|
+
# tool is being invoked with a long flag. So the predicate is: the tool name
|
|
27
|
+
# appears as a WORD, and some long flag appears. Nothing else is inspected.
|
|
28
|
+
#
|
|
29
|
+
# PREDICATE CHOICE (unchanged, and why TOOLS is a list of TOOLS): flag
|
|
30
|
+
# vocabularies are owned upstream and rot at every release, so the hook keys on
|
|
31
|
+
# the small, slow-moving set of tool NAMES. The same reasoning retired the
|
|
32
|
+
# parser: predicate on an invariant you own, never on someone else's vocabulary
|
|
33
|
+
# or grammar.
|
|
34
|
+
#
|
|
35
|
+
# NON-BLOCKING by design; NOT a security boundary (obfuscated invocations are out
|
|
36
|
+
# of scope, same declared class as the sibling hooks).
|
|
37
|
+
#
|
|
38
|
+
# ONE FIRE PER (session, tool): the first advisory for a tool in a session is
|
|
39
|
+
# enough of a nudge; the agent can check the whole command line from there. And
|
|
40
|
+
# within one Bash call only the FIRST listed tool is named — `glab ... && gh ...`
|
|
41
|
+
# yields one advisory, about glab. The reminder says to check the flags in this
|
|
42
|
+
# command, not just the named subcommand, so a second one would add nothing but
|
|
43
|
+
# noise; naming every tool would also mean deciding where each invocation begins,
|
|
44
|
+
# which is the parsing this hook exists without.
|
|
45
|
+
#
|
|
46
|
+
# ACCEPTED residuals — do NOT chase these with more string heuristics; chasing
|
|
47
|
+
# them is precisely what produced the fifteen-round parser:
|
|
48
|
+
# - FALSE FIRE: a command that merely MENTIONS a listed tool alongside a long
|
|
49
|
+
# flag (a heredoc body, a commit message whose metacharacters defeat the
|
|
50
|
+
# quote mask, a grep pattern) draws one advisory for that session. One extra
|
|
51
|
+
# line of context; the text says to ignore it if the flag is already known.
|
|
52
|
+
# - FALSE FIRE: a help invocation that is not the command's first word
|
|
53
|
+
# (`cd /x && glab mr list --help`) still draws the advisory.
|
|
54
|
+
# - NON-FIRE: a payload hidden inside a whitespace-bearing quoted string
|
|
55
|
+
# (`sh -c 'glab mr list --state opened'`). The quote mask exists to stop
|
|
56
|
+
# commit messages and grep patterns from firing, and it cannot tell those
|
|
57
|
+
# from a launcher payload without parsing the command — which is the thing
|
|
58
|
+
# this hook does not do. Accepted in the QUIET direction: a missed nudge, not
|
|
59
|
+
# a wrong one.
|
|
60
|
+
# - DUPLICATE FIRE: two Bash calls using the same tool that start concurrently
|
|
61
|
+
# can both pass the marker check before either append lands, so both advise.
|
|
62
|
+
# The dedup is best-effort by construction and a lock is not worth buying for
|
|
63
|
+
# a duplicated one-line nudge.
|
|
64
|
+
# - NON-FIRE: short flags, and any tool outside TOOLS. Extend the list only
|
|
65
|
+
# when a tool accumulates recorded flag failures.
|
|
66
|
+
# - NON-FIRE: payloads over the 1 MiB read cap are ignored entirely.
|
|
67
|
+
#
|
|
68
|
+
# Degrade semantics (per hooks/AGENTS.md): jq missing → emit nothing (exit 0);
|
|
69
|
+
# any internal issue → stay silent rather than disrupt the session.
|
|
70
|
+
|
|
71
|
+
set -f
|
|
72
|
+
|
|
73
|
+
# Bound the READ, not just the parse: this hook sits in front of every Bash call,
|
|
74
|
+
# so its cost must not scale with command size. Past the cap jq sees truncated
|
|
75
|
+
# JSON, yields nothing, and the hook exits silently.
|
|
76
|
+
input=$(head -c 1048576)
|
|
77
|
+
|
|
78
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
79
|
+
|
|
80
|
+
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
81
|
+
[ -z "$cmd" ] && exit 0
|
|
82
|
+
|
|
83
|
+
# Mask whitespace-bearing quoted spans so an ordinary commit message or grep
|
|
84
|
+
# pattern mentioning a tool does not fire. Single-word quoted values keep their
|
|
85
|
+
# content. This is the only text transform the hook performs.
|
|
86
|
+
masked=$(printf '%s' "$cmd" | sed -E \
|
|
87
|
+
-e 's/"([^"[:space:];|&<>()]*)"/\1/g' \
|
|
88
|
+
-e "s/'([^'[:space:];|&<>()]*)'/\\1/g" \
|
|
89
|
+
-e 's/"[^"]*"/QUOTED/g' \
|
|
90
|
+
-e "s/'[^']*'/QUOTED/g")
|
|
91
|
+
|
|
92
|
+
TOOLS='glab|gh|lark-cli|uv'
|
|
93
|
+
|
|
94
|
+
# EVERY listed tool in the command, not just the first. Taking only the first
|
|
95
|
+
# meant a tool that always trails another — `glab ... && gh ...` — could never
|
|
96
|
+
# reach its own advisory once the leader was deduped, so it stayed silent for the
|
|
97
|
+
# whole session. The dedup step below picks the first of these not yet advised.
|
|
98
|
+
# Still no parsing: this is the set of tool words present, not their positions.
|
|
99
|
+
tools_present=$(printf '%s' "$masked" | grep -Eow "(${TOOLS})" | awk '!seen[$0]++')
|
|
100
|
+
[ -z "$tools_present" ] && exit 0
|
|
101
|
+
tool=$(printf '%s' "$tools_present" | head -1)
|
|
102
|
+
|
|
103
|
+
printf '%s' "$masked" | grep -Eq -- '(^|[[:space:]])--[a-zA-Z][a-zA-Z0-9-]+' || exit 0
|
|
104
|
+
|
|
105
|
+
# Suppress only the unambiguous case: the command's FIRST WORD is the tool and it
|
|
106
|
+
# is asking for help. Taking one token is not parsing the command; it keeps the
|
|
107
|
+
# hook from telling someone to run the help they are already running, without a
|
|
108
|
+
# whole-string help check. Suppression is additionally limited to commands with
|
|
109
|
+
# NO shell separator: in `glab mr list --state opened && git --help` the first
|
|
110
|
+
# word is still the tool, so a whole-command help grep swallowed the advisory for
|
|
111
|
+
# a genuine invocation. Testing for a separator is one character class, not a
|
|
112
|
+
# parse — a compound command simply never qualifies for suppression.
|
|
113
|
+
first=$(printf '%s' "$masked" | awk '{print $1; exit}')
|
|
114
|
+
# A NEWLINE separates commands just as `;` does, and grep is line-oriented so a
|
|
115
|
+
# character class can never see one — a two-line command read as "simple" and its
|
|
116
|
+
# second line's `--help` suppressed the advisory for the first line's invocation.
|
|
117
|
+
# A standalone `--` hands the rest of the line to another program, so a `--help`
|
|
118
|
+
# after it belongs to that program, not to this tool: `uv run --python 3.12 --
|
|
119
|
+
# python --help` still needs the advisory for `--python`.
|
|
120
|
+
sep_lines=$(printf '%s' "$masked" | grep -c '')
|
|
121
|
+
if [ "$first" = "$tool" ] \
|
|
122
|
+
&& [ "$sep_lines" -le 1 ] \
|
|
123
|
+
&& ! printf '%s' "$masked" | grep -Eq -- '(^|[[:space:]])--([[:space:]]|$)' \
|
|
124
|
+
&& ! printf '%s' "$masked" | grep -q '[;|&]'; then
|
|
125
|
+
# `-h` / `--help` anywhere in this simple command, or a bare `help` only in the
|
|
126
|
+
# SUBCOMMAND slot. Accepting a bare `help` anywhere matched positional
|
|
127
|
+
# arguments — `gh api help --paginate` asks for an endpoint named `help`, not
|
|
128
|
+
# for help — and swallowed the advisory for a genuine flag.
|
|
129
|
+
second=$(printf '%s' "$masked" | awk '{print $2; exit}')
|
|
130
|
+
{ printf '%s' "$masked" | grep -Eq -- '(^|[[:space:]])(-h|--help)([[:space:]]|$)' \
|
|
131
|
+
|| [ "$second" = "help" ]; } && exit 0
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
# Best-effort per-session dedup. Any obstacle → skip dedup and remind again;
|
|
135
|
+
# reminding twice is harmless, the failure modes below are not.
|
|
136
|
+
#
|
|
137
|
+
# HOSTILE-TMPDIR HARDENING (adversarial challenge; every attack below was
|
|
138
|
+
# reproduced first-hand before being fixed). A predictable marker path directly
|
|
139
|
+
# under TMPDIR let another user on a shared /tmp pre-create it as a SYMLINK (the
|
|
140
|
+
# append wrote into an arbitrary file), as a FIFO (the append blocked until the
|
|
141
|
+
# host killed the hook at its 10s timeout — a denial of service on the agent's
|
|
142
|
+
# own Bash tool), or as a HARD LINK (which passes -f, -O and the symlink test
|
|
143
|
+
# alike). Sanitizing the session id does not help; the path is guessable either
|
|
144
|
+
# way. So the marker lives inside a directory this hook creates mode 0700 and
|
|
145
|
+
# verifies it owns, where another user cannot plant anything.
|
|
146
|
+
#
|
|
147
|
+
# The append runs inside a subshell whose stderr is already redirected, which
|
|
148
|
+
# closes the ENTIRE failed-open class — unwritable file, immutable flag, full
|
|
149
|
+
# filesystem, a state change between the checks and the open. An explicit `-w`
|
|
150
|
+
# check was removed once mutation showed it changed nothing the subshell did not
|
|
151
|
+
# already cover; keeping it would also have hidden the subshell from every probe
|
|
152
|
+
# by short-circuiting first. Enumerating open-failure causes is the same mistake
|
|
153
|
+
# as enumerating shell forms.
|
|
154
|
+
#
|
|
155
|
+
# THREAT MODEL, stated so this stops being re-litigated one finding at a time.
|
|
156
|
+
# The marker is a temp file holding a tool name. A successful attack on it buys
|
|
157
|
+
# exactly one thing: getting this user's uid to append a fixed short string
|
|
158
|
+
# (`glab`) to a file the attacker picks but cannot write themselves, or making
|
|
159
|
+
# the advisory silent. It cannot execute anything, cannot read anything, and
|
|
160
|
+
# carries no attacker-chosen content. The precondition walk below is therefore
|
|
161
|
+
# sized to that payload: resolve the physical path once, refuse the whole
|
|
162
|
+
# mechanism where the path is not exclusively ours or root's, and otherwise stop.
|
|
163
|
+
# The ancestor check was rewritten three times as review found successive holes —
|
|
164
|
+
# mode bits only, then bits plus sticky, then ownership, then physical
|
|
165
|
+
# resolution — which is the same recurrence signal the parser produced. The
|
|
166
|
+
# convergent form is the ownership invariant plus a physical path; further
|
|
167
|
+
# hardening of a fixed-string append is not worth more rounds.
|
|
168
|
+
#
|
|
169
|
+
# UNVERIFIED PROPERTIES (labelled, not counted as audited). Both need a second
|
|
170
|
+
# user id to construct, so no portable probe here reaches either, and a mutation
|
|
171
|
+
# removing each one leaves the suite fully green:
|
|
172
|
+
# - the `-O` cross-uid rejection on the marker directory;
|
|
173
|
+
# - the third-party-ownership rejection in the ancestor walk (a directory owned
|
|
174
|
+
# by another non-root user, whose owner may rename its entries regardless of
|
|
175
|
+
# the mode bits).
|
|
176
|
+
# The suite covers the same-uid and no-sticky directions of both. Do not read its
|
|
177
|
+
# green as covering these; they rest on code inspection alone.
|
|
178
|
+
# The suite does carry a PRESENCE check for each of them — a grep proving the
|
|
179
|
+
# guard is still written — so deleting one silently turns the suite red even
|
|
180
|
+
# though its behavior stays unverified. Presence is not behavior; the check
|
|
181
|
+
# exists only so an unverified guard cannot also become an unnoticed one.
|
|
182
|
+
session=$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null)
|
|
183
|
+
if [ -n "$session" ]; then
|
|
184
|
+
tmp_root="${TMPDIR:-/tmp}"
|
|
185
|
+
# PRECONDITION, because the TOCTOU below cannot be closed from bash. Every
|
|
186
|
+
# check on the marker directory is followed by a separate open of a path
|
|
187
|
+
# underneath it, and in a directory another user may write to they can rename
|
|
188
|
+
# the validated directory away and drop their own in its place between the two.
|
|
189
|
+
# Closing that needs a directory handle and openat-relative operations, which
|
|
190
|
+
# this language does not have. So the hook declines to dedup where dedup cannot
|
|
191
|
+
# be made safe, rather than pretending the window is shut: if the temp root is
|
|
192
|
+
# group- or world-writable and NOT sticky, skip the marker entirely and remind
|
|
193
|
+
# again. Sticky is what makes the usual shared /tmp safe — only the owner may
|
|
194
|
+
# rename or remove their entries there — and a per-user temp root is not
|
|
195
|
+
# other-writable at all, so this fires in neither normal case.
|
|
196
|
+
# Walk the whole ANCESTOR CHAIN, not just the temp root: a mode-0700 TMPDIR
|
|
197
|
+
# sitting under a world-writable non-sticky parent is swappable wholesale, so
|
|
198
|
+
# checking one level proves nothing about the path. Bounded by path depth, and
|
|
199
|
+
# `/` is never other-writable so it always terminates.
|
|
200
|
+
# Walk the PHYSICAL path. `dirname` on a logical path never resolves symlinks
|
|
201
|
+
# or `..`, so a relative TMPDIR or one whose target sits under a hostile
|
|
202
|
+
# ancestor walked a chain that does not exist on disk. Resolve once, up front.
|
|
203
|
+
# `cd -- ` because a TMPDIR of `-P` or `-L` is otherwise taken as a cd OPTION,
|
|
204
|
+
# leaving cd with no operand and landing in HOME — after which the ownership
|
|
205
|
+
# checks pass and the marker directory gets created there.
|
|
206
|
+
tmp_root=$(cd -- "$tmp_root" 2>/dev/null && pwd -P) || tmp_root=""
|
|
207
|
+
probe_dir="$tmp_root"
|
|
208
|
+
[ -z "$probe_dir" ] && probe_dir="/nonexistent"
|
|
209
|
+
walk_n=0
|
|
210
|
+
while : ; do
|
|
211
|
+
dir_perm=$(ls -ld "$probe_dir" 2>/dev/null | cut -c1-10)
|
|
212
|
+
if [ -z "$dir_perm" ]; then tmp_root=""; break; fi
|
|
213
|
+
# TWO independent ways an ancestor lets someone move the path out from under
|
|
214
|
+
# us, and the first is not about permission bits at all:
|
|
215
|
+
# (1) it is OWNED by a third party — a directory owner may rename entries
|
|
216
|
+
# in their own directory whatever the mode says, so 0755 owned by
|
|
217
|
+
# another non-root user is just as exploitable as 0777;
|
|
218
|
+
# (2) it is group/other-writable and NOT sticky — the classic shared-/tmp
|
|
219
|
+
# hole, where any writer may rename anyone's entry.
|
|
220
|
+
# Earlier versions checked only the mode bits and then only bits-plus-sticky,
|
|
221
|
+
# and independent review walked in through the gap each time. Ownership is
|
|
222
|
+
# the invariant; the bits are the secondary case.
|
|
223
|
+
dir_owner=$(ls -ld "$probe_dir" 2>/dev/null | awk '{print $3}')
|
|
224
|
+
if [ ! -O "$probe_dir" ] && [ "$dir_owner" != "root" ]; then
|
|
225
|
+
tmp_root=""; break
|
|
226
|
+
fi
|
|
227
|
+
if { [ "$(printf '%s' "$dir_perm" | cut -c6)" = "w" ] \
|
|
228
|
+
|| [ "$(printf '%s' "$dir_perm" | cut -c9)" = "w" ]; } \
|
|
229
|
+
&& [ ! -k "$probe_dir" ]; then
|
|
230
|
+
tmp_root=""; break
|
|
231
|
+
fi
|
|
232
|
+
# Bound the walk: a legitimate path can have hundreds of components and each
|
|
233
|
+
# iteration spawns several processes, in front of every Bash call. Real temp
|
|
234
|
+
# roots are shallow; past the bound, refuse rather than keep paying.
|
|
235
|
+
walk_n=$((walk_n + 1))
|
|
236
|
+
if [ "$walk_n" -gt 40 ]; then tmp_root=""; break; fi
|
|
237
|
+
# `--` for the same reason `cd` needed it: a path component that looks like
|
|
238
|
+
# an option is taken as one. Not reachable from a `pwd -P` result today, but
|
|
239
|
+
# the guard costs nothing and the omission is the exact shape already found
|
|
240
|
+
# once in this file.
|
|
241
|
+
parent_dir=$(dirname -- "$probe_dir")
|
|
242
|
+
[ "$parent_dir" = "$probe_dir" ] && break
|
|
243
|
+
probe_dir="$parent_dir"
|
|
244
|
+
done
|
|
245
|
+
marker_dir="${tmp_root}/ccl-skills-cliflag"
|
|
246
|
+
# The explicit chmod is load-bearing: `mkdir -p -m 700` applies its mode only
|
|
247
|
+
# when it CREATES the directory, so an already-present group/world-writable one
|
|
248
|
+
# keeps its permissions and leaves the swap window open.
|
|
249
|
+
if [ -n "$tmp_root" ] \
|
|
250
|
+
&& mkdir -p -m 700 "$marker_dir" 2>/dev/null \
|
|
251
|
+
&& [ -d "$marker_dir" ] && [ ! -L "$marker_dir" ] && [ -O "$marker_dir" ] \
|
|
252
|
+
&& chmod 700 "$marker_dir" 2>/dev/null; then
|
|
253
|
+
# Bound the NAME: an over-long session id became an over-long filename, and
|
|
254
|
+
# `>>file 2>/dev/null` applies redirections left to right, so the failed open
|
|
255
|
+
# printed before the suppression took effect.
|
|
256
|
+
safe_session=$(printf '%s' "$session" | tr -c 'A-Za-z0-9._-' '_' | cut -c1-100)
|
|
257
|
+
marker="${marker_dir}/${safe_session}"
|
|
258
|
+
marker_links=1
|
|
259
|
+
if [ -e "$marker" ]; then
|
|
260
|
+
marker_links=$(ls -ld "$marker" 2>/dev/null | awk '{print $2}')
|
|
261
|
+
case "$marker_links" in ''|*[!0-9]*) marker_links=99 ;; esac
|
|
262
|
+
fi
|
|
263
|
+
if [ -L "$marker" ] || [ "$marker_links" -gt 1 ] \
|
|
264
|
+
|| { [ -e "$marker" ] && { [ ! -f "$marker" ] || [ ! -O "$marker" ]; }; }; then
|
|
265
|
+
: # unexpected object — skip dedup entirely, do not open it
|
|
266
|
+
else
|
|
267
|
+
# SIZE-capped, and the story of why is worth keeping. This cap was added,
|
|
268
|
+
# then removed on the argument that the line-count bound below already
|
|
269
|
+
# prevented the append — an argument checked only against a MANY-LINE file,
|
|
270
|
+
# where it happens to hold. A single enormous LINE has a line count of 1,
|
|
271
|
+
# sails past that bound, gets appended to, and is re-scanned by both greps
|
|
272
|
+
# on every matching call. Varying one dimension of the input and concluding
|
|
273
|
+
# over all of them is the exact mistake the round's own material names.
|
|
274
|
+
# Anything past a few lines of tool names is not ours: skip it, unread.
|
|
275
|
+
marker_bytes=$(wc -c "$marker" 2>/dev/null | awk '{print $1; exit}')
|
|
276
|
+
case "$marker_bytes" in ''|*[!0-9]*) marker_bytes=0 ;; esac
|
|
277
|
+
if [ "$marker_bytes" -gt 4096 ]; then
|
|
278
|
+
# Oversized: neither read nor written. Guarding only the read left the
|
|
279
|
+
# append running, which is how a single enormous line kept growing.
|
|
280
|
+
:
|
|
281
|
+
else
|
|
282
|
+
# Advise the first tool in this command not yet recorded this session; if
|
|
283
|
+
# every one of them is recorded, stay quiet.
|
|
284
|
+
if [ -f "$marker" ]; then
|
|
285
|
+
tool=""
|
|
286
|
+
for candidate in $tools_present; do
|
|
287
|
+
grep -Fqx "$candidate" "$marker" 2>/dev/null || { tool="$candidate"; break; }
|
|
288
|
+
done
|
|
289
|
+
[ -z "$tool" ] && exit 0
|
|
290
|
+
fi
|
|
291
|
+
# `grep -c ''` on an EMPTY file prints 0 but EXITS 1, so trusting the
|
|
292
|
+
# exit code produced "0\n0" and a stderr diagnostic from the test below.
|
|
293
|
+
marker_lines=$(grep -c '' "$marker" 2>/dev/null | head -1)
|
|
294
|
+
case "$marker_lines" in ''|*[!0-9]*) marker_lines=0 ;; esac
|
|
295
|
+
if [ "$marker_lines" -lt 32 ]; then
|
|
296
|
+
( printf '%s\n' "$tool" >>"$marker" ) 2>/dev/null || true
|
|
297
|
+
fi
|
|
298
|
+
fi
|
|
299
|
+
fi
|
|
300
|
+
fi
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
advisory="🔎 CLI flag 核验提醒(本会话对 \`${tool}\` 只提示一次):\`${tool}\` 的 flag 词表**跨版本/跨平台差异很大**,凭记忆用 flag 是本仓记录中最高频的机械失败类之一——同一个 \`glab mr list --state\` 在 8 个独立会话里各踩一次,每次都只修了当次那一条。
|
|
304
|
+
这条命令里的 flag 若**本会话还没在本机确认过**,先读一次对应子命令的 help 再用。
|
|
305
|
+
已确认过就照常执行,忽略本提示。命令因无法识别的 flag 失败时,先怀疑这版不支持它,而不是先改用法之外的东西。"
|
|
306
|
+
|
|
307
|
+
jq -nc --arg r "$advisory" \
|
|
308
|
+
'{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:$r}}'
|
|
309
|
+
exit 0
|