Skip to content

Commit dd0a276

Browse files
authored
Merge pull request #12 from flexion/security/ensure-install-safe
fix: harden install-flex against injection, input validation, and supply-chain risks
2 parents a6d8168 + 1a9fb34 commit dd0a276

5 files changed

Lines changed: 173 additions & 37 deletions

File tree

.husky/pre-push

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
#!/bin/sh
22
set -e
3+
4+
# Run prek hooks (if installed)
5+
if command -v prek >/dev/null 2>&1; then
6+
prek run --stage pre-push
7+
fi
8+
39
# Check if bun version matches package.json
410
# keep in sync with packages/script/src/index.ts semver qualifier
511
bun -e '

.pre-commit-config.yaml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# See https://pre-commit.com for more information
2+
# See https://pre-commit.com/hooks.html for more hooks
3+
repos:
4+
# Standard pre-commit hooks
5+
- repo: https://github.com/pre-commit/pre-commit-hooks
6+
rev: v6.0.0
7+
hooks:
8+
- id: trailing-whitespace
9+
- id: end-of-file-fixer
10+
- id: check-yaml
11+
- id: check-json
12+
exclude: 'tsconfig\.json$|\.oxlintrc\.json$'
13+
- id: check-toml
14+
- id: check-added-large-files
15+
args: [ "--maxkb=1000" ]
16+
- id: detect-aws-credentials
17+
args: [ '--allow-missing-credentials' ]
18+
19+
# Detect secrets with GitLeaks
20+
- repo: https://github.com/zricethezav/gitleaks
21+
rev: v8.30.1
22+
hooks:
23+
- id: gitleaks-docker
24+
25+
# Lint GitHub Actions
26+
- repo: https://github.com/rhysd/actionlint
27+
rev: v1.7.10
28+
hooks:
29+
- id: actionlint-docker
30+
31+
- repo: https://github.com/lalten/check-gha-pinning
32+
rev: v1.3.1
33+
hooks:
34+
- id: check-gha-pinning
35+
36+
# Block forbidden files (.env, etc.)
37+
- repo: local
38+
hooks:
39+
- id: block-env-files
40+
name: Block .env files
41+
entry: scripts/block-env-files.sh
42+
language: script
43+
pass_filenames: false
44+
always_run: true

CONTRIBUTING.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,46 @@ https://github.com/anomalyco/models.dev
3939
bun dev
4040
```
4141

42+
### Setting Up Pre-Commit Hooks
43+
44+
Pre-commit hooks automatically validate code before pushing. This includes:
45+
- Secret detection (GitLeaks)
46+
- GitHub Actions linting (actionlint, GHA pinning)
47+
- Standard checks (trailing whitespace, JSON/YAML validation, etc.)
48+
- `.env` file protection
49+
50+
We use [prek](https://prek.j178.dev/), a fast Rust-based drop-in replacement for pre-commit.
51+
52+
To install prek:
53+
54+
```bash
55+
# Using uv (recommended)
56+
uv tool install prek
57+
58+
# Using pip
59+
pip install prek
60+
61+
# Using Homebrew
62+
brew install prek
63+
64+
# Or via standalone installer
65+
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/j178/prek/releases/latest/download/prek-installer.sh | sh
66+
```
67+
68+
Then install the git hooks:
69+
70+
```bash
71+
prek install
72+
```
73+
74+
This integrates prek into the `pre-push` Husky hook. Hooks will run automatically when you push. To manually run:
75+
76+
```bash
77+
prek run --stage pre-push # Run all hooks
78+
prek run --stage pre-push --files <file> # Run on specific file
79+
prek run gitleaks-docker --stage pre-push # Run single hook
80+
```
81+
4282
### Running against a different directory
4383

4484
By default, `bun dev` runs OpenCode in the `packages/opencode` directory. To run it against a different directory or repository:

install-flex

Lines changed: 77 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,12 @@ check_prereqs() {
4646
local missing=()
4747
command -v git >/dev/null 2>&1 || missing+=("git")
4848
command -v bun >/dev/null 2>&1 || missing+=("bun (https://bun.sh)")
49-
command -v aws >/dev/null 2>&1 || missing+=("aws-cli v2 (https://aws.amazon.com/cli/)")
49+
if command -v aws >/dev/null 2>&1; then
50+
aws --version 2>&1 | grep -q '^aws-cli/2\.' \
51+
|| missing+=("aws-cli v2 (found v1 — upgrade: https://aws.amazon.com/cli/)")
52+
else
53+
missing+=("aws-cli v2 (https://aws.amazon.com/cli/)")
54+
fi
5055
if [ ${#missing[@]} -gt 0 ]; then
5156
die "Missing prerequisites:$(printf '\n • %s' "${missing[@]}")"
5257
fi
@@ -62,36 +67,62 @@ gather_inputs() {
6267
printf 'Clone directory [%s]: ' "$HOME/opencode" >/dev/tty
6368
IFS= read -r CLONE_DIR </dev/tty
6469
CLONE_DIR="${CLONE_DIR:-$HOME/opencode}"
70+
# Expand leading ~ to $HOME before validation and use
71+
CLONE_DIR="${CLONE_DIR/#\~/$HOME}"
72+
# Reject shell metacharacters that could corrupt the rc file
73+
case "$CLONE_DIR" in
74+
*['|;&$`<>(){}!^*?'\\]*)
75+
die "Invalid clone directory — shell metacharacters are not allowed" ;;
76+
esac
77+
# Reject control characters (especially newlines)
78+
if [[ "$CLONE_DIR" =~ [[:cntrl:]] ]]; then
79+
die "Invalid clone directory — control characters are not allowed"
80+
fi
6581

6682
while true; do
6783
printf 'AWS account ID: ' >/dev/tty
6884
IFS= read -r ACCOUNT_ID </dev/tty
69-
[ -n "${ACCOUNT_ID:-}" ] && break
70-
warn "AWS account ID is required."
85+
[ -n "${ACCOUNT_ID:-}" ] || { warn "AWS account ID is required."; continue; }
86+
# AWS account IDs are exactly 12 digits
87+
[[ "$ACCOUNT_ID" =~ ^[0-9]{12}$ ]] || { warn "AWS account ID must be exactly 12 digits."; continue; }
88+
break
7189
done
7290

7391
printf 'Preferred AWS region [us-east-1]: ' >/dev/tty
7492
IFS= read -r AWS_REGION </dev/tty
7593
AWS_REGION="${AWS_REGION:-us-east-1}"
94+
# AWS regions match the pattern: two-or-more-letters, dash, letters, dash, digit(s)
95+
[[ "$AWS_REGION" =~ ^[a-z]{2,}-[a-z]+-[0-9]+$ ]] \
96+
|| die "Invalid AWS region format (expected e.g. us-east-1, eu-west-2)"
7697
echo
7798
}
7899

79100
# ── step 3: clone & build ─────────────────────────────────────────────────────
80101
clone_and_build() {
81102
if [ -d "$CLONE_DIR/.git" ]; then
82103
warn "Directory $CLONE_DIR already exists — skipping clone"
83-
info "Fetching and checking out $BRANCH branch..."
104+
# Refuse to proceed over uncommitted changes to avoid silent data loss.
105+
if ! git -C "$CLONE_DIR" diff --quiet || ! git -C "$CLONE_DIR" diff --cached --quiet; then
106+
die "$CLONE_DIR has uncommitted changes — commit or stash them and re-run"
107+
fi
108+
local current
109+
current=$(git -C "$CLONE_DIR" rev-parse --abbrev-ref HEAD)
110+
if [ "$current" != "$BRANCH" ]; then
111+
die "$CLONE_DIR is on branch '$current', not '$BRANCH' — switch manually and re-run"
112+
fi
113+
info "Fetching latest $BRANCH branch..."
84114
git -C "$CLONE_DIR" fetch origin
85-
git -C "$CLONE_DIR" checkout "$BRANCH"
115+
git -C "$CLONE_DIR" merge --ff-only "origin/$BRANCH"
86116
else
87117
info "Cloning $REPO_URL$CLONE_DIR ..."
88-
git clone --branch "$BRANCH" "$REPO_URL" "$CLONE_DIR"
118+
git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$CLONE_DIR"
89119
fi
90120

91121
info "Installing dependencies..."
92122
# BUN_CONFIG_REGISTRY override prevents private registry redirects (e.g. CMS Artifactory)
93123
# from breaking public package resolution.
94-
(cd "$CLONE_DIR" && BUN_CONFIG_REGISTRY=https://registry.npmjs.org bun install)
124+
# --frozen-lockfile ensures bun.lock is respected and no unexpected version upgrades occur.
125+
(cd "$CLONE_DIR" && BUN_CONFIG_REGISTRY=https://registry.npmjs.org bun install --frozen-lockfile)
95126

96127
info "Building opencode binary (this takes about a minute)..."
97128
# OPENCODE_CHANNEL=flex ensures every Flexion fork build — regardless of which git
@@ -107,23 +138,20 @@ clone_and_build() {
107138
# ── step 4: AWS SSO config ────────────────────────────────────────────────────
108139
write_aws_config() {
109140
mkdir -p "$HOME/.aws"
110-
local config="$HOME/.aws/config"
111-
112-
if grep -q "\[profile $AWS_PROFILE\]" "$config" 2>/dev/null; then
113-
warn "AWS profile [$AWS_PROFILE] already in $config — skipping"
141+
# Use aws configure set for atomic, structured writes — avoids raw cat >> which can
142+
# corrupt ~/.aws/config if a concurrent write leaves an unterminated section header.
143+
# aws configure get doubles as an idempotency check without relying on fragile grep.
144+
if aws configure get sso_start_url --profile "$AWS_PROFILE" >/dev/null 2>&1; then
145+
warn "AWS profile [$AWS_PROFILE] already configured — skipping"
114146
return
115147
fi
116148

117-
info "Writing AWS SSO profile to $config..."
118-
cat >>"$config" <<EOF
119-
120-
[profile $AWS_PROFILE]
121-
sso_start_url = $SSO_START_URL
122-
sso_region = $SSO_REGION
123-
sso_account_id = $ACCOUNT_ID
124-
sso_role_name = $SSO_ROLE_NAME
125-
region = $AWS_REGION
126-
EOF
149+
info "Writing AWS SSO profile via aws configure set..."
150+
aws configure set sso_start_url "$SSO_START_URL" --profile "$AWS_PROFILE"
151+
aws configure set sso_region "$SSO_REGION" --profile "$AWS_PROFILE"
152+
aws configure set sso_account_id "$ACCOUNT_ID" --profile "$AWS_PROFILE"
153+
aws configure set sso_role_name "$SSO_ROLE_NAME" --profile "$AWS_PROFILE"
154+
aws configure set region "$AWS_REGION" --profile "$AWS_PROFILE"
127155
success "AWS profile [$AWS_PROFILE] written"
128156
}
129157

@@ -191,6 +219,7 @@ write_opencode_config() {
191219
}
192220
}
193221
EOF
222+
chmod 600 "$config_file"
194223
success "opencode config written"
195224
}
196225

@@ -202,35 +231,46 @@ write_shell_alias() {
202231
*) rc_file="$HOME/.bashrc" ;;
203232
esac
204233

205-
if grep -q "opencode-work()" "$rc_file" 2>/dev/null; then
234+
# Sentinel comment is the idempotency guard — more reliable than matching the
235+
# function signature, which a user might remove while leaving a stale comment.
236+
if grep -qF "── Flexion opencode launcher ──" "$rc_file" 2>/dev/null; then
206237
warn "opencode-work() already defined in $rc_file — skipping"
207238
return
208239
fi
209240

210241
info "Appending opencode-work() to $rc_file..."
211242

212-
# Single-quoted heredoc prevents variable expansion inside the function body.
213-
# CLONE_DIR_PLACEHOLDER is substituted after writing via perl.
214-
cat >>"$rc_file" <<'SHELL_EOF'
243+
# Use printf %q to shell-quote the clone path at install time.
244+
# This eliminates the previous Perl s|...|..| substitution which was vulnerable
245+
# to injection if CLONE_DIR contained a | character.
246+
local clone_dir_q
247+
clone_dir_q=$(printf '%q' "$CLONE_DIR")
248+
249+
# Double-quoted heredoc: $clone_dir_q expands now (install time);
250+
# all other $ references are \-escaped so they expand at shell run time.
251+
cat >>"$rc_file" <<SHELL_EOF
215252
216253
# ── Flexion opencode launcher ─────────────────────────────────────────────────
217254
opencode-work() {
218255
local profile="ClaudeCodeAccess"
256+
local clone_dir=$clone_dir_q
219257
local arch os
220-
case "$(uname -m)" in arm64|aarch64) arch="arm64" ;; *) arch="x64" ;; esac
221-
case "$(uname -s)" in Darwin) os="darwin" ;; Linux) os="linux" ;; *)
222-
echo "Unsupported OS: $(uname -s)" && return 1 ;; esac
223-
echo "Logging in to AWS SSO ($profile)..."
224-
aws sso login --profile "$profile" || return 1
225-
eval "$(aws configure export-credentials --profile "$profile" --format env)"
226-
"CLONE_DIR_PLACEHOLDER/packages/opencode/dist/opencode-${os}-${arch}/bin/opencode" "$@"
258+
case "\$(uname -m)" in arm64|aarch64) arch="arm64" ;; *) arch="x64" ;; esac
259+
case "\$(uname -s)" in Darwin) os="darwin" ;; Linux) os="linux" ;; *)
260+
echo "Unsupported OS: \$(uname -s)" && return 1 ;; esac
261+
echo "Logging in to AWS SSO (\$profile)..."
262+
aws sso login --profile "\$profile" || return 1
263+
# Subshell confines exported credentials to the opencode process — they do not
264+
# persist in the interactive shell environment after opencode exits.
265+
(
266+
eval "\$(aws configure export-credentials --profile "\$profile" --format env)"
267+
export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
268+
"\${clone_dir}/packages/opencode/dist/opencode-\${os}-\${arch}/bin/opencode" "\$@"
269+
)
227270
}
228271
# ─────────────────────────────────────────────────────────────────────────────
229272
SHELL_EOF
230273

231-
# Substitute actual clone path. Uses | as delimiter to safely handle / in paths.
232-
perl -i -pe "s|CLONE_DIR_PLACEHOLDER|${CLONE_DIR}|g" "$rc_file"
233-
234274
success "opencode-work() added to $rc_file"
235275
}
236276

@@ -245,8 +285,8 @@ main() {
245285

246286
local rc_file
247287
case "$(basename "${SHELL:-bash}")" in
248-
zsh) rc_file="~/.zshrc" ;;
249-
*) rc_file="~/.bashrc" ;;
288+
zsh) rc_file="$HOME/.zshrc" ;;
289+
*) rc_file="$HOME/.bashrc" ;;
250290
esac
251291

252292
printf '\n%s%sInstallation complete!%s\n' "$GREEN" "$BOLD" "$NC"

scripts/block-env-files.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/bash
2+
# Block .env files from being committed
3+
if git diff --cached --name-only | grep -qE '^\.(env|env\..*)$'; then
4+
echo "Error: .env files cannot be committed"
5+
exit 1
6+
fi

0 commit comments

Comments
 (0)