Skip to content

Commit 998912d

Browse files
committed
Harden shell scripts, fix input sanitization, and improve error handling
- Fix CRITICAL CACHE_DIR bug: use ${:=} so empty plist values trigger default, add whitespace trimming, switch to allowlist prefix check ($HOME/.cache) - Fix jq regex injection: replace test() with ascii_downcase+contains via --arg - Fix search query injection: switch from denylist to allowlist sanitization - Replace fragile tr/sed JSON assembly with jq -s slurp and jq -c output - Add mktemp error handling, signal-safe temp file cleanup - Add per_page=100 to /user/repos for fewer API calls with --paginate - Add API_HOST hostname validation and jq availability check in setup.sh - Surface API errors via stderr instead of silently suppressing with 2>/dev/null - Add final jq fallback to always output valid Alfred JSON - Replace echo -n with printf for portability - Add API_HOST to variablesdontexport to prevent leaking enterprise hostnames - Fix release.yml: stricter semver tag pattern, HEAD instead of HEAD^1, first-tag changelog fallback - Fix build.sh: add nullglob/dotglob for cleanup, include gh.png in artifact - Add CACHE_DIR to info.plist variables - Fix plist > → > for proper blockquote rendering - Sync README and plist readme wording fixes
1 parent d2f1f74 commit 998912d

File tree

8 files changed

+194
-77
lines changed

8 files changed

+194
-77
lines changed

.github/workflows/release.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: Build and release
33
on:
44
push:
55
tags:
6-
- '*.*.*'
6+
- '[0-9]+.[0-9]+.[0-9]+'
77

88
jobs:
99
release:
@@ -22,7 +22,13 @@ jobs:
2222
run: bash build.sh
2323

2424
- name: Generate Changelog
25-
run: git log $(git tag --sort=-creatordate | awk 'NR==2')..HEAD^1 --oneline --no-merges --no-decorate > CHANGELOG
25+
run: |
26+
prev_tag=$(git tag --sort=-creatordate | awk 'NR==2')
27+
if [[ -n "$prev_tag" ]]; then
28+
git log "${prev_tag}..HEAD" --oneline --no-merges --no-decorate > CHANGELOG
29+
else
30+
git log HEAD --oneline --no-merges --no-decorate > CHANGELOG
31+
fi
2632

2733
- name: Create GitHub Release
2834
uses: softprops/action-gh-release@v2

README.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ brew install gh
1515
```
1616

1717
> 💡 **Note**:
18-
> The command above assume you use [Homebrew](https://brew.sh) as your package manager.
18+
> The command above assumes you use [Homebrew](https://brew.sh) as your package manager.
1919

2020
## Authentication
2121

@@ -34,11 +34,11 @@ Double click on the `.alfredworkflow` file and follow the instructions.
3434

3535
## Usage
3636

37-
There's a single keyword that triggers the workflow: `gh`
37+
The main keyword that triggers the workflow is: `gh`
3838

3939
Then you can start typing the name of the repository you're looking for.
40-
It will first try to search within your user's repositories. And if no result
41-
is found, then it'll search in all public repositories.
40+
It will first fetch your repositories and filter them locally by name. If no
41+
match is found, it will search GitHub repositories.
4242

4343
```
4444
gh octocat/hello-world
@@ -47,23 +47,23 @@ gh octocat/hello-world
4747
![Example of gh command](gh.png)
4848

4949
When an item is highlighted, you can press Enter to open the repository's page,
50-
or press any of the following modifiers keys for other options:
50+
or press any of the following modifier keys for other options:
5151

5252
### Hold `Ctrl ⌃` for repository actions page
5353

5454
Press Enter while holding down the `Ctrl` key to open the repository's actions page.
5555

5656
### Hold `Cmd ⌘` to see Pull Requests
5757

58-
Press Enter while holding down the `Cmd` key to list the repository's open PR's.
58+
Press Enter while holding down the `Cmd` key to list the repository's open PRs.
5959

60-
### `Option ⌥` modifier
60+
### Hold `Option ⌥` to copy SSH clone command
6161

6262
Press Enter while holding down the `Option` key to copy the clone command with the repository's SSH URL.
6363

64-
### `Shift+Option ⇧+⌥` modifier
64+
### Hold `Shift+Option ⇧+⌥` to copy HTTPS clone command
6565

66-
Press Enter while holding down the `Shift+Option` keys to copy the clone command with the repository's clone URL.
66+
Press Enter while holding down the `Shift+Option` keys to copy the clone command with the repository's HTTPS URL.
6767

6868
## Configuration
6969

@@ -73,17 +73,17 @@ You can configure the cache duration passed to the GitHub CLI, by setting the fo
7373

7474
| Environment Variable | Description | Default |
7575
| -------------------- | ---------------------------------------- | ----------------- |
76-
| `CACHE_PULLS` | Cache duration for PR's API call | `10m` |
76+
| `CACHE_PULLS` | Cache duration for PRs API call | `10m` |
7777
| `CACHE_SEARCH_REPOS` | Cache duration for repos search API call | `24h` |
7878
| `CACHE_USER_REPOS` | Cache duration for user repos API call | `72h` |
79-
| `CACHE_DIR` | Cache directory for the the `gh` CLI | `$HOME/.cache/gh` |
79+
| `CACHE_DIR` | Directory removed by `ghclear` command | `$HOME/.cache/gh` |
8080

8181
> ⚠️ **Caution** ⚠️
8282
>
83-
> If you don't see your recently created repository in the results, it may be the cache duration mentioned above.
83+
> If you don't see your recently created repository in the results, it may be due to the cache duration mentioned above.
8484
>
8585
> Also make sure to use absolute paths if you need to customize the default.
86-
> Like `/Users/juan/cache` instead `$HOME/cache`
86+
> Like `/Users/juan/cache` instead of `$HOME/cache`
8787

8888
To clear the cache and force a new request to the GitHub API, type this in Alfred:
8989

build.sh

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
#!/bin/bash -e
1+
#!/bin/bash
2+
set -euo pipefail
23

34
OUTPUT="github-repos.alfredworkflow"
5+
rm -rf build
46
mkdir -p build
5-
cp clear-cache.sh gh.sh icon.png info.plist pulls.sh setup.sh build/
7+
cp clear-cache.sh gh.sh gh.png icon.png info.plist pulls.sh setup.sh build/
68
cd build
7-
zip $OUTPUT -r . -qq
8-
ls | grep -v "$OUTPUT" | xargs rm -r
9+
zip "$OUTPUT" -r . -qq
10+
shopt -s nullglob dotglob
11+
for f in *; do [[ "$f" != "$OUTPUT" ]] && rm -rf "$f"; done
912
echo "Done -> $OUTPUT"

clear-cache.sh

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,29 @@
11
#!/bin/bash
22

3-
CACHE_DIR="${CACHE_DIR:-"$HOME/.cache/gh"}"
3+
# Use := so empty string AND unset both trigger the default
4+
CACHE_DIR="${CACHE_DIR:="$HOME/.cache/gh"}"
5+
6+
# Trim whitespace
7+
CACHE_DIR=$(printf '%s' "$CACHE_DIR" | tr -d '[:space:]')
8+
9+
# Resolve symlinks and .. segments to canonical path (macOS compatible)
10+
if [[ -d "$CACHE_DIR" ]]; then
11+
CACHE_DIR=$(cd "$CACHE_DIR" && /bin/pwd -P)
12+
fi
13+
14+
# Block empty, root, or home-level paths
15+
if [[ -z "$CACHE_DIR" || "$CACHE_DIR" == "/" ]]; then
16+
printf '%s' "Error: refusing to delete unsafe path: ${CACHE_DIR}" >&2
17+
exit 1
18+
fi
19+
20+
# Require path is under $HOME/.cache (allowlist approach)
21+
expected_prefix="$HOME/.cache"
22+
if [[ "$CACHE_DIR" != "$expected_prefix"/* && "$CACHE_DIR" != "$expected_prefix" ]]; then
23+
printf '%s' "Error: CACHE_DIR must be under ${expected_prefix}: ${CACHE_DIR}" >&2
24+
exit 1
25+
fi
426

527
rm -rf "$CACHE_DIR"
628

7-
echo -n "${CACHE_DIR}"
29+
printf '%s' "${CACHE_DIR}"

gh.sh

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
#!/bin/bash
22

3-
source ./setup.sh
3+
source "$(dirname "$0")/setup.sh"
44

55
query=$1
66
item=$(
7-
cat <<EOF
7+
cat <<'EOF'
88
{
99
uid: .id,
1010
title: .full_name,
@@ -36,23 +36,79 @@ item=$(
3636
EOF
3737
)
3838

39+
empty_result='{"items":[]}'
40+
41+
err=$(mktemp) || {
42+
printf '%s' "$empty_result"
43+
exit 0
44+
}
45+
3946
repos=$(gh api /user/repos --method GET \
4047
-f sort=pushed \
48+
-F per_page=100 \
4149
--hostname "$API_HOST" \
4250
--cache "$CACHE_USER_REPOS" \
4351
--paginate \
44-
--jq ".[] | $item" | grep -i "$query")
45-
46-
if [[ -z "$repos" ]]; then
47-
repos=$(gh api /search/repositories --method GET \
48-
--hostname "$API_HOST" \
49-
-f q="$query in:name archived:false" \
50-
-F per_page=9 \
51-
-f sort=pushed \
52-
--cache "$CACHE_SEARCH_REPOS" \
53-
--jq ".items.[] | $item")
52+
--jq "[.[] | $item]" 2>"$err")
53+
gh_exit=$?
54+
err_msg=$(<"$err")
55+
rm -f "$err"
56+
57+
if [[ $gh_exit -ne 0 ]]; then
58+
if [[ -n "$err_msg" ]]; then
59+
printf '%s\n' "$err_msg" >&2
60+
fi
61+
printf '%s' "$empty_result"
62+
exit 0
5463
fi
5564

56-
items=$(echo -n "$repos" | tr '\n', ',' | sed 's/,$//')
65+
# --paginate outputs one JSON array per page; merge them and optionally filter
66+
if [[ -n "$query" ]]; then
67+
# Use --arg to safely pass query into jq (no injection possible)
68+
repos=$(printf '%s\n' "$repos" | jq -s --arg q "$query" \
69+
'[add // [] | .[] | select(.title | ascii_downcase | contains($q | ascii_downcase))]') \
70+
|| repos="[]"
71+
else
72+
repos=$(printf '%s\n' "$repos" | jq -s 'add // []') || repos="[]"
73+
fi
74+
75+
# Check if we got results
76+
has_results=$(printf '%s' "$repos" | jq 'length > 0' 2>/dev/null)
77+
78+
# Fall back to search if no local match and query is present
79+
if [[ "$has_results" != "true" && -n "$query" ]]; then
80+
# Sanitize query: allowlist of safe characters only
81+
safe_query=$(printf '%s' "$query" | tr -cd 'a-zA-Z0-9 ._-')
82+
83+
if [[ -n "$safe_query" ]]; then
84+
err=$(mktemp) || {
85+
printf '%s' "$empty_result"
86+
exit 0
87+
}
88+
89+
repos=$(gh api /search/repositories --method GET \
90+
--hostname "$API_HOST" \
91+
-f q="$safe_query in:name archived:false" \
92+
-F per_page=9 \
93+
-f sort=pushed \
94+
--cache "$CACHE_SEARCH_REPOS" \
95+
--jq "[.items[] | $item]" 2>"$err")
96+
gh_exit=$?
97+
err_msg=$(<"$err")
98+
rm -f "$err"
99+
100+
if [[ $gh_exit -ne 0 ]]; then
101+
if [[ -n "$err_msg" ]]; then
102+
printf '%s\n' "$err_msg" >&2
103+
fi
104+
repos="[]"
105+
fi
106+
fi
107+
108+
if [[ -z "$repos" ]]; then
109+
repos="[]"
110+
fi
111+
fi
57112

58-
echo -n "{\"items\":[$items]}"
113+
# Final output with jq fallback
114+
printf '%s' "$repos" | jq -c '{items: .}' 2>/dev/null || printf '%s' "$empty_result"

info.plist

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -384,8 +384,8 @@ Install the GitHub CLI:
384384
brew install gh
385385
```
386386

387-
&gt; **Note**:
388-
&gt; The command above assume you use [Homebrew](https://brew.sh) as your package manager.
387+
> **Note**:
388+
> The command above assumes you use [Homebrew](https://brew.sh) as your package manager.
389389

390390
## Authentication
391391

@@ -398,11 +398,11 @@ gh auth login
398398

399399
## Usage
400400

401-
There's a single keyword that triggers the workflow: `gh`
401+
The main keyword that triggers the workflow is: `gh`
402402

403403
Then you can start typing the name of the repository you're looking for.
404-
It will first try to search within your user's repositories. And if no result
405-
is found, then it'll search in all public repositories.
404+
It will first fetch your repositories and filter them locally by name. If no
405+
match is found, it will search GitHub repositories.
406406

407407
```
408408
gh octocat/hello-world
@@ -411,23 +411,23 @@ gh octocat/hello-world
411411
![Example of gh command](gh.png)
412412

413413
When an item is highlighted, you can press Enter to open the repository's page,
414-
or press any of the following modifiers keys for other options:
414+
or press any of the following modifier keys for other options:
415415

416416
### Hold `Ctrl ⌃` for repository actions page
417417

418418
Press Enter while holding down the `Ctrl` key to open the repository's actions page.
419419

420420
### Hold `Cmd ⌘` to see Pull Requests
421421

422-
Press Enter while holding down the `Cmd` key to list the repository's open PR's.
422+
Press Enter while holding down the `Cmd` key to list the repository's open PRs.
423423

424-
### `Option ⌥` modifier
424+
### Hold `Option ⌥` to copy SSH clone command
425425

426426
Press Enter while holding down the `Option` key to copy the clone command with the repository's SSH URL.
427427

428-
### `Shift+Option ⇧+⌥` modifier
428+
### Hold `Shift+Option ⇧+⌥` to copy HTTPS clone command
429429

430-
Press Enter while holding down the `Shift+Option` keys to copy the clone command with the repository's clone URL.
430+
Press Enter while holding down the `Shift+Option` keys to copy the clone command with the repository's HTTPS URL.
431431

432432
## Configuration
433433

@@ -437,17 +437,17 @@ You can configure the cache duration passed to the GitHub CLI, by setting the fo
437437

438438
| Environment Variable | Description | Default |
439439
| -------------------- | ---------------------------------------- | ----------------- |
440-
| `CACHE_PULLS` | Cache duration for PR's API call | `10m` |
440+
| `CACHE_PULLS` | Cache duration for PRs API call | `10m` |
441441
| `CACHE_SEARCH_REPOS` | Cache duration for repos search API call | `24h` |
442442
| `CACHE_USER_REPOS` | Cache duration for user repos API call | `72h` |
443-
| `CACHE_DIR` | Cache directory for the the `gh` CLI | `$HOME/.cache/gh` |
443+
| `CACHE_DIR` | Directory removed by `ghclear` command | `$HOME/.cache/gh` |
444444

445-
&gt; ⚠️ **Caution** ⚠️
446-
&gt;
447-
&gt; If you don't see your recently created repository in the results, it may be the cache duration mentioned above.
448-
&gt;
449-
&gt; Also make sure to use absolute paths if you need to customize the default.
450-
&gt; Like `/Users/juan/cache` instead `$HOME/cache`
445+
> ⚠️ **Caution** ⚠️
446+
>
447+
> If you don't see your recently created repository in the results, it may be due to the cache duration mentioned above.
448+
>
449+
> Also make sure to use absolute paths if you need to customize the default.
450+
> Like `/Users/juan/cache` instead of `$HOME/cache`
451451

452452
To clear the cache and force a new request to the GitHub API, type this in Alfred:
453453

@@ -547,9 +547,13 @@ This project is published under the [MIT License](LICENSE.md).</string>
547547
<string>24h</string>
548548
<key>CACHE_USER_REPOS</key>
549549
<string>72h</string>
550+
<key>CACHE_DIR</key>
551+
<string></string>
550552
</dict>
551553
<key>variablesdontexport</key>
552-
<array/>
554+
<array>
555+
<string>API_HOST</string>
556+
</array>
553557
<key>version</key>
554558
<string>4.0.0</string>
555559
<key>webaddress</key>

0 commit comments

Comments
 (0)