Skip to content

Commit 17f4162

Browse files
using-systemclaude
andauthored
feat(git-commit): add default branch guard rule (#3)
* feat(git-commit): add default branch guard rule Prevent direct commits on main/master by requiring a conventional branch (feat/, fix/, perf/, etc.) to be created first. This enforces a cleaner git workflow aligned with Conventional Commits. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(readme): add marketplace scope to plugin install commands The plugin install commands were missing the @using-system marketplace scope, which is required to resolve the plugin from the marketplace. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(github-actions): add script injection prevention rule Require passing ${{ }} expressions through environment variables instead of interpolating them directly in run blocks. This prevents command injection via user-controlled inputs like PR titles, issue bodies, and branch names. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(ci): add release workflow with automated version bump Adds a GitHub Actions release workflow triggered on push to main that: - Determines next semver version via github-tag-action (Conventional Commits) - Bumps version in all files referenced by .version-bump.json - Audits for stale version references - Creates a GitHub Release with changelog Also enhances the github-actions skill with guidance on job outputs injection safety and with: vs run: context distinction. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a4d8e61 commit 17f4162

5 files changed

Lines changed: 254 additions & 4 deletions

File tree

.github/workflows/release.yml

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
version:
13+
name: Determine version
14+
runs-on: ubuntu-latest
15+
outputs:
16+
new_tag: ${{ steps.tag.outputs.new_tag }}
17+
new_version: ${{ steps.tag.outputs.new_version }}
18+
changelog: ${{ steps.tag.outputs.changelog }}
19+
previous_tag: ${{ steps.tag.outputs.previous_tag }}
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
23+
with:
24+
fetch-depth: 0
25+
26+
- name: Determine next version
27+
id: tag
28+
uses: mathieudutour/github-tag-action@d28fa2ccfbd16e871a4bdf35e11b3ad1bd56c0c1 # v6.2
29+
with:
30+
github_token: ${{ secrets.GITHUB_TOKEN }}
31+
default_bump: patch
32+
tag_prefix: v
33+
34+
bump:
35+
name: Bump version in files
36+
runs-on: ubuntu-latest
37+
needs: version
38+
if: needs.version.outputs.new_tag != ''
39+
steps:
40+
- name: Checkout
41+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
42+
with:
43+
ref: main
44+
45+
- name: Bump versions from .version-bump.json
46+
env:
47+
NEW_VERSION: ${{ needs.version.outputs.new_version }}
48+
run: |
49+
echo "Bumping version to ${NEW_VERSION}"
50+
51+
jq -c '.files[]' .version-bump.json | while IFS= read -r entry; do
52+
FILE=$(echo "$entry" | jq -r '.path')
53+
FIELD=$(echo "$entry" | jq -r '.field')
54+
55+
if [ ! -f "$FILE" ]; then
56+
echo "::warning::File not found: $FILE"
57+
continue
58+
fi
59+
60+
# Convert dot-notation path to jq setpath array
61+
# e.g. "plugins.0.version" -> ["plugins",0,"version"]
62+
JQ_PATH=$(echo "$FIELD" | jq -R 'split(".") | map(if test("^[0-9]+$") then tonumber else . end)')
63+
64+
echo "Updating $FILE field '$FIELD' to $NEW_VERSION"
65+
jq --arg v "$NEW_VERSION" --argjson path "$JQ_PATH" 'setpath($path; $v)' "$FILE" > tmp.$$.json && mv tmp.$$.json "$FILE"
66+
done
67+
68+
- name: Audit for stale version references
69+
env:
70+
PREVIOUS_TAG: ${{ needs.version.outputs.previous_tag }}
71+
run: |
72+
PREVIOUS_VERSION="${PREVIOUS_TAG#v}"
73+
74+
if [ -z "$PREVIOUS_VERSION" ]; then
75+
echo "No previous version to audit against"
76+
exit 0
77+
fi
78+
79+
echo "Auditing for stale references to ${PREVIOUS_VERSION}"
80+
81+
EXCLUDE_ARGS=""
82+
for pattern in $(jq -r '.audit.exclude[]' .version-bump.json); do
83+
EXCLUDE_ARGS="$EXCLUDE_ARGS --exclude=$pattern --exclude-dir=$pattern"
84+
done
85+
86+
if grep -r "$PREVIOUS_VERSION" . $EXCLUDE_ARGS --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' 2>/dev/null; then
87+
echo "::warning::Found stale version references to ${PREVIOUS_VERSION} — check the files above"
88+
else
89+
echo "No stale version references found"
90+
fi
91+
92+
- name: Commit and push version bump
93+
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
94+
with:
95+
commit_message: "chore(release): bump version to v${{ needs.version.outputs.new_version }}"
96+
file_pattern: "*.json **/*.json"
97+
98+
release:
99+
name: Create GitHub release
100+
runs-on: ubuntu-latest
101+
needs: [version, bump]
102+
steps:
103+
- name: Create release
104+
uses: actions/github-script@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0
105+
env:
106+
NEW_TAG: ${{ needs.version.outputs.new_tag }}
107+
CHANGELOG: ${{ needs.version.outputs.changelog }}
108+
with:
109+
script: |
110+
await github.rest.repos.createRelease({
111+
owner: context.repo.owner,
112+
repo: context.repo.repo,
113+
tag_name: process.env.NEW_TAG,
114+
name: process.env.NEW_TAG,
115+
body: process.env.CHANGELOG,
116+
draft: false,
117+
prerelease: false
118+
});

.version-bump.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"files": [
3+
{ "path": "package.json", "field": "version" },
4+
{ "path": ".claude-plugin/plugin.json", "field": "version" },
5+
{ "path": ".cursor-plugin/plugin.json", "field": "version" },
6+
{ "path": ".claude-plugin/marketplace.json", "field": "plugins.0.version" }
7+
],
8+
"audit": {
9+
"exclude": [
10+
"CHANGELOG.md",
11+
"node_modules",
12+
".git",
13+
".version-bump.json"
14+
]
15+
}
16+
}

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ A curated collection of personal agent extensions, skills, and CLI hooks to enha
1616

1717
```bash
1818
claude plugin marketplace add using-system/ai-dev-extensions
19-
claude plugin install ai-dev-extensions
19+
claude plugin install ai-dev-extensions@using-system
2020
```
2121

2222
Or for a single session from a local clone:
@@ -29,14 +29,14 @@ claude --plugin-dir /path/to/ai-dev-extensions
2929

3030
```bash
3131
/plugin marketplace add using-system/ai-dev-extensions
32-
/plugin install ai-dev-extensions
32+
/plugin install ai-dev-extensions@using-system
3333
```
3434

3535
### Copilot CLI
3636

3737
```bash
3838
copilot plugin marketplace add using-system/ai-dev-extensions
39-
copilot plugin install ai-dev-extensions
39+
copilot plugin install ai-dev-extensions@using-system
4040
```
4141

4242
### Codex

skills/git-commit/SKILL.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,55 @@ Breaking changes trigger a **MAJOR** version bump.
5252

5353
## Rules
5454

55+
### 0. Never commit directly on the default branch
56+
57+
Before committing, check the current branch:
58+
59+
```bash
60+
git branch --show-current
61+
```
62+
63+
If you are on the default branch (`main` or `master`), you **MUST** create and switch to a new branch before committing. The branch name MUST follow the convention:
64+
65+
```
66+
<type>/<short-description>
67+
```
68+
69+
Where `<type>` matches the Conventional Commits type of the work being done.
70+
71+
```bash
72+
# BAD - committing on main
73+
git commit -m "feat: add login page"
74+
75+
# GOOD - create branch first, then commit
76+
git checkout -b feat/add-login-page
77+
git commit -m "feat: add login page"
78+
```
79+
80+
#### Branch naming rules
81+
82+
- Use the same type as the commit (`feat`, `fix`, `perf`, `docs`, `refactor`, `ci`, `test`, `build`, `chore`)
83+
- Description is lowercase, words separated by hyphens
84+
- Keep it short but descriptive (3-5 words max)
85+
86+
```bash
87+
# Examples
88+
feat/oauth2-login-flow
89+
fix/null-response-payment
90+
perf/optimize-query-cache
91+
docs/add-installation-guide
92+
refactor/extract-auth-middleware
93+
ci/pin-actions-to-sha
94+
```
95+
96+
#### Detecting the default branch
97+
98+
Use this command to determine the default branch:
99+
100+
```bash
101+
git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@refs/remotes/origin/@@' || echo "main"
102+
```
103+
55104
### 1. Type is mandatory
56105

57106
```bash

skills/github-actions/SKILL.md

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,74 @@ The inline comment after the SHA MUST contain the human-readable version so that
6262

6363
Format: `{owner}/{action}@{sha} # {tag}`
6464

65-
### 4. Apply to ALL actions, including official ones
65+
### 4. Never use expressions directly in `run` scripts — use environment variables
66+
67+
Interpolating `${{ }}` expressions directly in `run:` blocks (bash, PowerShell, or any shell) creates **script injection vulnerabilities**. An attacker who controls the expression value (e.g., a PR title, branch name, or issue body) can inject arbitrary commands.
68+
69+
**Always** pass untrusted values through environment variables:
70+
71+
```yaml
72+
# BAD - direct interpolation, vulnerable to command injection
73+
- run: echo "Hello ${{ github.event.pull_request.title }}"
74+
75+
# BAD - same issue in PowerShell
76+
- run: Write-Host "Hello ${{ github.event.issue.body }}"
77+
shell: pwsh
78+
79+
# GOOD - pass through env, shell handles escaping
80+
- run: echo "Hello $TITLE"
81+
env:
82+
TITLE: ${{ github.event.pull_request.title }}
83+
84+
# GOOD - PowerShell equivalent
85+
- run: Write-Host "Hello $env:BODY"
86+
shell: pwsh
87+
env:
88+
BODY: ${{ github.event.issue.body }}
89+
```
90+
91+
#### Dangerous expression sources
92+
93+
These expressions are **user-controlled** and MUST always go through `env:`:
94+
95+
| Source | Example |
96+
|--------|---------|
97+
| PR title / body | `github.event.pull_request.title`, `github.event.pull_request.body` |
98+
| Issue title / body | `github.event.issue.title`, `github.event.issue.body` |
99+
| Comment body | `github.event.comment.body` |
100+
| Commit message | `github.event.head_commit.message` |
101+
| Branch / tag name | `github.head_ref`, `github.ref_name` |
102+
| Review body | `github.event.review.body` |
103+
104+
#### Job outputs (`needs.*.outputs.*`)
105+
106+
Job outputs are only as safe as how they were produced. If a job output originates from user-controlled data (commit messages, PR titles, branch names) — even indirectly via a third-party action — it MUST go through `env:` when used in `run:` or `script:` blocks.
107+
108+
```yaml
109+
# BAD - output may contain injected content from commit messages
110+
- run: echo "Releasing ${{ needs.version.outputs.new_tag }}"
111+
112+
# GOOD - passed through env
113+
- run: echo "Releasing ${NEW_TAG}"
114+
env:
115+
NEW_TAG: ${{ needs.version.outputs.new_tag }}
116+
117+
# OK - with: inputs are action parameters, not shell-interpolated
118+
with:
119+
commit_message: "chore(release): v${{ needs.version.outputs.new_version }}"
120+
```
121+
122+
**Note**: `with:` inputs are passed as strings to actions, not shell-interpolated — they are safe from script injection. The `env:` rule applies specifically to `run:` and `script:` blocks.
123+
124+
#### Safe expressions (env still recommended)
125+
126+
These are generally safe but using `env:` is still best practice for consistency:
127+
128+
- `github.repository`, `github.actor`, `github.sha`
129+
- `github.run_id`, `github.run_number`
130+
- `secrets.*`, `vars.*`
131+
132+
### 5. Apply to ALL actions, including official ones
66133

67134
This applies to **every** action, including:
68135
- `actions/checkout`

0 commit comments

Comments
 (0)