From c2366cc2d5eb3e678b8a6e64842475faad3802d8 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Thu, 11 Dec 2025 03:42:40 +0900 Subject: [PATCH 01/41] feat(fish): update _kyber_function to use Tailscale for SSH access --- .../programs/fish/functions/_kyber_function.fish | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/home-manager/programs/fish/functions/_kyber_function.fish b/home-manager/programs/fish/functions/_kyber_function.fish index a90a09a56..b68f2eb61 100644 --- a/home-manager/programs/fish/functions/_kyber_function.fish +++ b/home-manager/programs/fish/functions/_kyber_function.fish @@ -1,9 +1,3 @@ -function _kyber_function --description "SSH to Kyber server" - set -l password (security find-generic-password -s "ssh ubuntu@91.242.214.231" -w 2>/dev/null) - if test -n "$password" - sshpass -p $password ssh ubuntu@$KYBER_IP_ADDR - else - echo "Password not found in Keychain. Run: security add-generic-password -s 'ssh ubuntu@91.242.214.231' -a ubuntu -w" - ssh ubuntu@$KYBER_IP_ADDR - end +function _kyber_function --description "SSH to Kyber server via Tailscale" + tailscale ssh ubuntu@kyber end From 14d4e3ada9d0fb63eb0c187b0c72aaf4d239b918 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 12 Dec 2025 00:16:20 +0900 Subject: [PATCH 02/41] feat(nix): update installation instructions to use Determinate Nix --- Makefile | 4 ++-- README-nix.md | 4 ++-- flake.lock | 36 ++++++++++++++++++------------------ install.sh | 12 ++++++------ named-hosts/kyber/README.md | 2 +- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/Makefile b/Makefile index 3d2fa75a4..eda4edb92 100644 --- a/Makefile +++ b/Makefile @@ -202,8 +202,8 @@ devenv-cli: ## Build the packaged devenv CLI binary. .PHONY: nix-install nix-install: ## Install Nix if not already installed. @if [ "$(NIX_ENV)" = "not_found" ]; then \ - echo "๐Ÿš€ Installing Nix environment for $(NIX_CONFIG_TYPE) on $(OS) $(ARCH) for USER=$(NIX_USERNAME)"; \ - curl -L https://nixos.org/nix/install | sh; \ + echo "๐Ÿš€ Installing Determinate Nix environment for $(NIX_CONFIG_TYPE) on $(OS) $(ARCH) for USER=$(NIX_USERNAME)"; \ + curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install; \ fi @echo "โœ… Nix environment installed!" diff --git a/README-nix.md b/README-nix.md index e91979e0c..7ce44362c 100644 --- a/README-nix.md +++ b/README-nix.md @@ -3,8 +3,8 @@ This repo can build and expose all CLI tools declared in `package.json` using the exact versions from the lockfile, via [dream2nix]. ## Prerequisites -- Nix installed -- Flakes enabled (see Nix docs) +- Nix installed (recommended: [Determinate Nix](https://determinate.systems/nix-installer/)) +- Flakes enabled (automatically enabled with Determinate Nix) ## Typical usage diff --git a/flake.lock b/flake.lock index f9e5d3df2..7b92969ec 100644 --- a/flake.lock +++ b/flake.lock @@ -92,11 +92,11 @@ ] }, "locked": { - "lastModified": 1764927628, - "narHash": "sha256-AH2H5O9i7k3oarg3MooAnQtZxo44qxrUTUuvGOy/OEc=", + "lastModified": 1765463314, + "narHash": "sha256-n271YQksEn8+KZ8JpNcP9Shw4uHh4zYsouw+asNFRiE=", "owner": "cachix", "repo": "devenv", - "rev": "247d7027f91368054fb0eefbd755a73d42b66fee", + "rev": "ec805a5ad3e3ffb6d47d05c078a60db2e636fac4", "type": "github" }, "original": { @@ -257,11 +257,11 @@ ] }, "locked": { - "lastModified": 1764998300, - "narHash": "sha256-fZatn/KLfHLDXnF0wy7JxXqGaZmGDTVufT4o/AOlj44=", + "lastModified": 1765461410, + "narHash": "sha256-AVZ1y5tfTGqf5zJx6uY52KHN2pP9gGF9RMQ3meBVKIg=", "owner": "nix-community", "repo": "home-manager", - "rev": "27a6182347ccae90a88231ae0dc5dfa7d15815bb", + "rev": "7b34e428f31ce52baabda53e52bf14002e1c0625", "type": "github" }, "original": { @@ -294,11 +294,11 @@ ] }, "locked": { - "lastModified": 1765152310, - "narHash": "sha256-uJD1VGkailVaIgljInof9xphRo6LeCFmLLBrXa16Sf8=", + "lastModified": 1765411465, + "narHash": "sha256-rwrGoscS1AXRsiB100JduKO9ySKEjC8zsSBIDfKi1G8=", "owner": "nix-community", "repo": "neovim-nightly-overlay", - "rev": "8631ec2f011d93a7ee31297cb0154de7893ef3a9", + "rev": "d47f5957732f763696803b564eb9c608a486014e", "type": "github" }, "original": { @@ -310,11 +310,11 @@ "neovim-src": { "flake": false, "locked": { - "lastModified": 1765138411, - "narHash": "sha256-pyxO9Gpts9tEwfcSFrVLL0gQy3MtIfify5PJHDCwAo8=", + "lastModified": 1765391613, + "narHash": "sha256-6tqSpj8b33kSY/0rYEdQRHsICdkZD/AtXSoC0EH/hFY=", "owner": "neovim", "repo": "neovim", - "rev": "551bb63d449f85f89dee8ee7f842274d4566fcc9", + "rev": "78bbe53f7615e8b38d5289d9ce0579996109579b", "type": "github" }, "original": { @@ -405,11 +405,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1764947035, - "narHash": "sha256-EYHSjVM4Ox4lvCXUMiKKs2vETUSL5mx+J2FfutM7T9w=", + "lastModified": 1765270179, + "narHash": "sha256-g2a4MhRKu4ymR4xwo+I+auTknXt/+j37Lnf0Mvfl1rE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a672be65651c80d3f592a89b3945466584a22069", + "rev": "677fbe97984e7af3175b6c121f3c39ee5c8d62c9", "type": "github" }, "original": { @@ -442,11 +442,11 @@ ] }, "locked": { - "lastModified": 1765157960, - "narHash": "sha256-JSRg0sccKLY82/LZBagowbBohEXy09bho7k/HdgDUxo=", + "lastModified": 1765458763, + "narHash": "sha256-iYiCd/8QNiThwxcl3CC5nR5emrAKM2hcB3YkWZ4yx0k=", "owner": "nix-community", "repo": "NUR", - "rev": "e9cb74edfb001abc89e68e27939881dedc8aaa7c", + "rev": "72bffdb9666817f9bae30ebfbf858a83de20776b", "type": "github" }, "original": { diff --git a/install.sh b/install.sh index b01a48687..429d8f996 100755 --- a/install.sh +++ b/install.sh @@ -42,18 +42,18 @@ fi if ! command -v nix >/dev/null 2>&1; then echo "Installing Nix..." if [ "$OS" = "macos" ]; then - curl -L https://nixos.org/nix/install | bash + curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install # For macOS, source the Nix profile immediately to update PATH in CI. . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh NIX_EFFECTIVE_BIN_PATH="/nix/var/nix/profiles/default/bin" else # Linux if [ "$IN_DOCKER" = "true" ]; then - echo "Performing single-user Nix installation (Docker environment)..." - curl -L https://nixos.org/nix/install | bash -s -- --no-daemon + echo "Performing Determinate Nix installation (Docker environment)..." + curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install linux --init none --no-confirm # Source the Nix profile script to add Nix to PATH for the current shell if [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then . "$HOME/.nix-profile/etc/profile.d/nix.sh" - echo "Sourced Nix profile for single-user (Docker) setup." + echo "Sourced Nix profile for Determinate Nix (Docker) setup." else echo "Warning: Nix profile script ($HOME/.nix-profile/etc/profile.d/nix.sh) not found after installation." # Fallback PATH export for the current shell @@ -61,8 +61,8 @@ if ! command -v nix >/dev/null 2>&1; then fi NIX_EFFECTIVE_BIN_PATH="$HOME/.nix-profile/bin" else # Linux multi-user - echo "Performing multi-user Nix installation..." - curl -L https://nixos.org/nix/install | bash -s -- --daemon + echo "Performing Determinate Nix multi-user installation..." + curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install linux # For Linux multi-user installations, add the default Nix path for the current shell. export PATH=/nix/var/nix/profiles/default/bin:$PATH NIX_EFFECTIVE_BIN_PATH="/nix/var/nix/profiles/default/bin" diff --git a/named-hosts/kyber/README.md b/named-hosts/kyber/README.md index b947c51d4..7e31cd332 100644 --- a/named-hosts/kyber/README.md +++ b/named-hosts/kyber/README.md @@ -13,7 +13,7 @@ sudo systemctl enable --now tailscaled sudo tailscale up # 2. Install Nix -curl -L https://nixos.org/nix/install | sh -s -- --daemon +curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install linux # 3. Clone dotfiles git clone https://github.com/shunkakinoki/dotfiles ~/dotfiles From 1f1a6c0fd8c183e13d65921beeff3686b583c440 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 12 Dec 2025 01:32:49 +0900 Subject: [PATCH 03/41] fix(flake.lock): update NUR lock details for consistency --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 7b92969ec..0ce371033 100644 --- a/flake.lock +++ b/flake.lock @@ -442,11 +442,11 @@ ] }, "locked": { - "lastModified": 1765458763, - "narHash": "sha256-iYiCd/8QNiThwxcl3CC5nR5emrAKM2hcB3YkWZ4yx0k=", + "lastModified": 1765464729, + "narHash": "sha256-zd3l8XbKZTyZDNgq04p6wG8zsQJqPS68gcQV2plLbq0=", "owner": "nix-community", "repo": "NUR", - "rev": "72bffdb9666817f9bae30ebfbf858a83de20776b", + "rev": "f8eff1f701449ea22541f93b8a9a162947aa0f7c", "type": "github" }, "original": { From 6a7d973651500d62577df6f9681720ab38e37eea Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 12 Dec 2025 03:03:32 +0900 Subject: [PATCH 04/41] feat(settings): enhance security by adding read/write deny rules for sensitive files --- config/claude/settings.json | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/config/claude/settings.json b/config/claude/settings.json index 96bc38568..6858bd9b2 100644 --- a/config/claude/settings.json +++ b/config/claude/settings.json @@ -37,7 +37,6 @@ "Bash(git status:*)", "Bash(git tag:*)", "Bash(git worktree:*)", - "Bash(go:*)", "Bash(grep:*)", "Bash(kubectl:*)", "Bash(lsof:*)", @@ -52,8 +51,6 @@ "Bash(pip:*)", "Bash(pip3:*)", "Bash(pnpm:*)", - "Bash(python:*)", - "Bash(python3:*)", "Bash(scutil:*)", "Bash(timeout:*)", "Bash(uv:*)", @@ -69,7 +66,30 @@ "Bash(chmod -R 777:*)", "Bash(mkfs:*)", "Bash(dd if=:*)" - ] + ], + "read": { + "deny": [ + "**/.env", + "**/.env.local", + "**/*.pem", + "**/*.key", + "**/secrets/**", + "**/credentials/**", + "**/.aws/**", + "**/.ssh/**", + "**/docker-compose*.yml", + "**/config/database.yml" + ] + }, + "write": { + "deny": [ + "**/secrets/**", + "**/credentials/**", + "**/.ssh/**", + "**/config/database.yml", + "**/docker-compose*.yml" + ] + } }, "hooks": { "UserPromptSubmit": [ From 7a2b982249177864cd5bcdb612d0cadad1b1f92a Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 12 Dec 2025 05:44:58 +0900 Subject: [PATCH 05/41] fix(config): update ampcode management restriction to allow remote access --- config/cliproxyapi/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/cliproxyapi/config.yaml b/config/cliproxyapi/config.yaml index 4fde4d2a8..3b6c68a3d 100644 --- a/config/cliproxyapi/config.yaml +++ b/config/cliproxyapi/config.yaml @@ -37,7 +37,7 @@ ws-auth: false # AMP ampcode: upstream-url: "https://ampcode.com" - restrict-management-to-localhost: true + restrict-management-to-localhost: false # amp-upstream-api-key: "" # Optional - use AMP_API_KEY env var or ~/.local/share/amp/secrets.json # Gemini API keys (preferred) From 1f505b0cc4b0a5b5e5353b9b63ec1b69d17b2362 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 12 Dec 2025 06:09:41 +0900 Subject: [PATCH 06/41] fix(config): restrict remote management access and enforce localhost-only settings --- config/cliproxyapi/config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/cliproxyapi/config.yaml b/config/cliproxyapi/config.yaml index 3b6c68a3d..99c36479f 100644 --- a/config/cliproxyapi/config.yaml +++ b/config/cliproxyapi/config.yaml @@ -4,7 +4,7 @@ port: 8317 remote-management: # Whether to allow remote (non-localhost) management access. # When false, only localhost can access management endpoints (a key is still required). - allow-remote: true + allow-remote: false # Management key. If a plaintext value is provided here, it will be hashed on startup. # All management requests (even from localhost) require this key. # Leave empty to disable the Management API entirely (404 for all /v0/management routes). @@ -37,7 +37,7 @@ ws-auth: false # AMP ampcode: upstream-url: "https://ampcode.com" - restrict-management-to-localhost: false + restrict-management-to-localhost: true # amp-upstream-api-key: "" # Optional - use AMP_API_KEY env var or ~/.local/share/amp/secrets.json # Gemini API keys (preferred) From 0343b1769d80a48a29ca7d6f78ea0737506697e6 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 12 Dec 2025 06:12:50 +0900 Subject: [PATCH 07/41] feat(config): inject CLIPROXY_MANAGEMENT_PASSWORD into config and update example env --- .env.example | 1 + config/cliproxyapi/config.yaml | 2 +- home-manager/services/cliproxyapi/start.sh | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 63be8df5d..c31a9a780 100644 --- a/.env.example +++ b/.env.example @@ -2,3 +2,4 @@ # Copy to home-manager/.env and provide real values. The .env file stays local. MY_SECRET=replace-me # GITHUB_TOKEN=ghp_your_token_here +CLIPROXY_MANAGEMENT_PASSWORD=your-management-key-here diff --git a/config/cliproxyapi/config.yaml b/config/cliproxyapi/config.yaml index 99c36479f..43c09dcfd 100644 --- a/config/cliproxyapi/config.yaml +++ b/config/cliproxyapi/config.yaml @@ -8,7 +8,7 @@ remote-management: # Management key. If a plaintext value is provided here, it will be hashed on startup. # All management requests (even from localhost) require this key. # Leave empty to disable the Management API entirely (404 for all /v0/management routes). - secret-key: "" + secret-key: "__CLIPROXY_MANAGEMENT_PASSWORD__" # Disable the bundled management control panel asset download and HTTP route when true. disable-control-panel: false # Authentication directory (supports ~ for home directory). If you use Windows, please set the directory like this: `C:/cli-proxy-api/` diff --git a/home-manager/services/cliproxyapi/start.sh b/home-manager/services/cliproxyapi/start.sh index d3cf15bac..5961214fd 100755 --- a/home-manager/services/cliproxyapi/start.sh +++ b/home-manager/services/cliproxyapi/start.sh @@ -17,7 +17,9 @@ fi # Generate config from template with secrets injected if [ -f "$TEMPLATE" ]; then - sed "s|__OPENROUTER_API_KEY__|${OPENROUTER_API_KEY:-}|g" "$TEMPLATE" >"$CONFIG" + sed -e "s|__OPENROUTER_API_KEY__|${OPENROUTER_API_KEY:-}|g" \ + -e "s|__CLIPROXY_MANAGEMENT_PASSWORD__|${CLIPROXY_MANAGEMENT_PASSWORD:-}|g" \ + "$TEMPLATE" >"$CONFIG" fi # Change to config dir so logs are created there From 476871feef88126f01bc0267702809ccf3cb34b9 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 12 Dec 2025 10:10:38 +0900 Subject: [PATCH 08/41] feat(fish): add _kybers_function for SSH to Kyber server via Tailscale --- home-manager/programs/fish/default.nix | 2 ++ home-manager/programs/fish/functions/_kybers_function.fish | 3 +++ 2 files changed, 5 insertions(+) create mode 100644 home-manager/programs/fish/functions/_kybers_function.fish diff --git a/home-manager/programs/fish/default.nix b/home-manager/programs/fish/default.nix index a75a0cf2f..80e13430c 100644 --- a/home-manager/programs/fish/default.nix +++ b/home-manager/programs/fish/default.nix @@ -60,6 +60,7 @@ grco = "_grco_function"; grcr = "_grcr_function"; kyber = "_kyber_function"; + kybers = "_kybers_function"; fch = "_fzf_cmd_history --allow-execute"; fdp = "_fzf_directory_picker --allow-cd --prompt-name Projects ~/"; ffp = "_fzf_file_picker --allow-open-in-editor --prompt-name Files"; @@ -133,6 +134,7 @@ "_grcr_function" "_hm_load_env_file" "_kyber_function" + "_kybers_function" "fish_user_key_bindings" ] ); diff --git a/home-manager/programs/fish/functions/_kybers_function.fish b/home-manager/programs/fish/functions/_kybers_function.fish new file mode 100644 index 000000000..acd8dae89 --- /dev/null +++ b/home-manager/programs/fish/functions/_kybers_function.fish @@ -0,0 +1,3 @@ +function _kybers_function --description "SSH to Kyber server via Tailscale with zellij" + tailscale ssh -t ubuntu@kyber zellij attach -c +end From afd3a6878e70391285b0d51aef837d16600c4334 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 12 Dec 2025 10:11:09 +0900 Subject: [PATCH 09/41] fix(Makefile): add launchctl to switch target for Nix configuration application --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index eda4edb92..79f75d9b5 100644 --- a/Makefile +++ b/Makefile @@ -135,7 +135,7 @@ setup: nix-setup ## Basic Nix setup (alias for nix-setup). setup-dev: nix-setup git-submodule-sync shell-install ## Set up local development environment (Nix + submodules + shell). .PHONY: switch -switch: nix-switch ## Apply Nix configuration and restart launchd agents. +switch: nix-switch launchctl ## Apply Nix configuration and restart launchd agents. .PHONY: test test: neovim-test From ec66ff53e7b92a9b09644691ac47314cc58b6b48 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 12 Dec 2025 10:13:13 +0900 Subject: [PATCH 10/41] fix(flake.lock): update locked versions and hashes for dependencies --- flake.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/flake.lock b/flake.lock index 0ce371033..313618d6c 100644 --- a/flake.lock +++ b/flake.lock @@ -147,11 +147,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1763759067, - "narHash": "sha256-LlLt2Jo/gMNYAwOgdRQBrsRoOz7BPRkzvNaI/fzXi2Q=", + "lastModified": 1765495779, + "narHash": "sha256-MhA7wmo/7uogLxiewwRRmIax70g6q1U/YemqTGoFHlM=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "2cccadc7357c0ba201788ae99c4dfa90728ef5e0", + "rev": "5635c32d666a59ec9a55cab87e898889869f7b71", "type": "github" }, "original": { @@ -168,11 +168,11 @@ ] }, "locked": { - "lastModified": 1763759067, - "narHash": "sha256-LlLt2Jo/gMNYAwOgdRQBrsRoOz7BPRkzvNaI/fzXi2Q=", + "lastModified": 1765495779, + "narHash": "sha256-MhA7wmo/7uogLxiewwRRmIax70g6q1U/YemqTGoFHlM=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "2cccadc7357c0ba201788ae99c4dfa90728ef5e0", + "rev": "5635c32d666a59ec9a55cab87e898889869f7b71", "type": "github" }, "original": { @@ -257,11 +257,11 @@ ] }, "locked": { - "lastModified": 1765461410, - "narHash": "sha256-AVZ1y5tfTGqf5zJx6uY52KHN2pP9gGF9RMQ3meBVKIg=", + "lastModified": 1765480374, + "narHash": "sha256-HlbvQAqLx7WqZFFQZ8nu5UUJAVlXiV/kqKbyueA8srw=", "owner": "nix-community", "repo": "home-manager", - "rev": "7b34e428f31ce52baabda53e52bf14002e1c0625", + "rev": "39cb677ed9e908e90478aa9fe5f3383dfc1a63f3", "type": "github" }, "original": { @@ -294,11 +294,11 @@ ] }, "locked": { - "lastModified": 1765411465, - "narHash": "sha256-rwrGoscS1AXRsiB100JduKO9ySKEjC8zsSBIDfKi1G8=", + "lastModified": 1765497880, + "narHash": "sha256-Bm3Llrq6vMh8o8kW0v8dCDuiXSpFCLOxXORIedLzOys=", "owner": "nix-community", "repo": "neovim-nightly-overlay", - "rev": "d47f5957732f763696803b564eb9c608a486014e", + "rev": "f8bf621897eb25a97aaf3f53e517e2bd83fbcffa", "type": "github" }, "original": { @@ -310,11 +310,11 @@ "neovim-src": { "flake": false, "locked": { - "lastModified": 1765391613, - "narHash": "sha256-6tqSpj8b33kSY/0rYEdQRHsICdkZD/AtXSoC0EH/hFY=", + "lastModified": 1765479520, + "narHash": "sha256-EI1iD5XmxwOkJSXc7Q1xCUIMWT84/1saYZ9ITPsDrzg=", "owner": "neovim", "repo": "neovim", - "rev": "78bbe53f7615e8b38d5289d9ce0579996109579b", + "rev": "054eaf8493cbcf35ad386675fcaa0cc38d6b63e7", "type": "github" }, "original": { @@ -442,11 +442,11 @@ ] }, "locked": { - "lastModified": 1765464729, - "narHash": "sha256-zd3l8XbKZTyZDNgq04p6wG8zsQJqPS68gcQV2plLbq0=", + "lastModified": 1765496834, + "narHash": "sha256-B8AnKiB4U1nW69aGsuLICwIQ3Qid8/ok9HZQAihBaHo=", "owner": "nix-community", "repo": "NUR", - "rev": "f8eff1f701449ea22541f93b8a9a162947aa0f7c", + "rev": "61bb240f28350baef6922c9d77405296e50440a7", "type": "github" }, "original": { From 4363561b84153384c372a1221c3092ac466dd8e3 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 12 Dec 2025 10:27:00 +0900 Subject: [PATCH 11/41] fix(start.sh): export MANAGEMENT_PASSWORD for Management API --- home-manager/services/cliproxyapi/start.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/home-manager/services/cliproxyapi/start.sh b/home-manager/services/cliproxyapi/start.sh index 5961214fd..02fa72dad 100755 --- a/home-manager/services/cliproxyapi/start.sh +++ b/home-manager/services/cliproxyapi/start.sh @@ -15,6 +15,9 @@ if [ -f "$ENV_FILE" ]; then set +a fi +# Export management password for Management API (CLIProxyAPI requires MANAGEMENT_PASSWORD env var) +export MANAGEMENT_PASSWORD="${CLIPROXY_MANAGEMENT_PASSWORD:-}" + # Generate config from template with secrets injected if [ -f "$TEMPLATE" ]; then sed -e "s|__OPENROUTER_API_KEY__|${OPENROUTER_API_KEY:-}|g" \ From 75688a17f43e6a9914b03620a2f019c01e345226 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 12 Dec 2025 10:49:00 +0900 Subject: [PATCH 12/41] feat(settings): add dangerouslySkipPermissions flag to model configuration --- config/claude/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/claude/settings.json b/config/claude/settings.json index 6858bd9b2..8d1d28c44 100644 --- a/config/claude/settings.json +++ b/config/claude/settings.json @@ -221,5 +221,6 @@ "type": "command", "command": "node -e \"const fs=require('fs'),path=require('path'),rl=require('readline');let i='';process.stdin.on('data',c=>i+=c);process.stdin.on('end',async()=>{try{const d=JSON.parse(i);const m=d.model?.display_name||'claude-sonnet-4';const dir=d.workspace?.current_dir?.replace(process.env.HOME,'~')||path.basename(process.cwd());const sid=d.session_id;let t=0;if(sid){const pdir=path.join(process.env.HOME,'.claude','projects');if(fs.existsSync(pdir)){for(const pd of fs.readdirSync(pdir).map(d=>path.join(pdir,d)).filter(d=>fs.statSync(d).isDirectory())){const tf=path.join(pd,sid+'.jsonl');if(fs.existsSync(tf)){const rs=fs.createReadStream(tf);const r=rl.createInterface({input:rs});let lu=null;r.on('line',l=>{try{const e=JSON.parse(l);if(e.type==='assistant'&&e.message?.usage)lu=e.message.usage}catch{}});await new Promise(res=>{r.on('close',()=>{if(lu)t=(lu.input_tokens||0)+(lu.output_tokens||0)+(lu.cache_creation_input_tokens||0)+(lu.cache_read_input_tokens||0);res()})});break}}}}const getContextLimit=m=>{if(m.includes('sonnet-4'))return 500000;if(m.includes('3-5-sonnet')||m.includes('sonnet-3.5'))return 200000;if(m.includes('haiku'))return 200000;if(m.includes('opus'))return 200000;return 200000};const cl=getContextLimit(m);const p=Math.min(100,Math.round(t/cl*100));const td=t>=1000000?(t/1000000).toFixed(1)+'M':t>=1000?(t/1000).toFixed(1)+'K':t.toString();const ld=cl>=1000000?(cl/1000000).toFixed(0)+'M':cl>=1000?(cl/1000).toFixed(0)+'K':cl.toString();const c=p>=90?'\\x1b[31m':p>=70?'\\x1b[33m':'\\x1b[32m';console.log('\\x1b[34m'+process.env.USER+'@'+require('os').hostname().split('.')[0]+'\\x1b[0m:\\x1b[34m'+dir+'\\x1b[0m ['+m+'] ๐Ÿช™ '+td+'/'+ld+' '+c+p+'%\\x1b[0m '+new Date().toLocaleTimeString('en-US',{hour12:false}))}catch{console.log('\\x1b[34m'+process.env.USER+'@'+require('os').hostname().split('.')[0]+'\\x1b[0m:\\x1b[34m'+(process.cwd().replace(process.env.HOME,'~'))+'\\x1b[0m [claude-sonnet-4] ๐Ÿช™ 0 \\x1b[31mโŒ\\x1b[0m '+new Date().toLocaleTimeString('en-US',{hour12:false}))}});\"" }, - "model": "sonnet" + "model": "sonnet", + "dangerouslySkipPermissions": true } From 81460101699d9d568dca18da5fea07946d544407 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 12 Dec 2025 10:52:50 +0900 Subject: [PATCH 13/41] fix(settings): replace dangerouslySkipPermissions with disableBypassPermissionsMode --- config/claude/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/claude/settings.json b/config/claude/settings.json index 8d1d28c44..64d82ac00 100644 --- a/config/claude/settings.json +++ b/config/claude/settings.json @@ -222,5 +222,5 @@ "command": "node -e \"const fs=require('fs'),path=require('path'),rl=require('readline');let i='';process.stdin.on('data',c=>i+=c);process.stdin.on('end',async()=>{try{const d=JSON.parse(i);const m=d.model?.display_name||'claude-sonnet-4';const dir=d.workspace?.current_dir?.replace(process.env.HOME,'~')||path.basename(process.cwd());const sid=d.session_id;let t=0;if(sid){const pdir=path.join(process.env.HOME,'.claude','projects');if(fs.existsSync(pdir)){for(const pd of fs.readdirSync(pdir).map(d=>path.join(pdir,d)).filter(d=>fs.statSync(d).isDirectory())){const tf=path.join(pd,sid+'.jsonl');if(fs.existsSync(tf)){const rs=fs.createReadStream(tf);const r=rl.createInterface({input:rs});let lu=null;r.on('line',l=>{try{const e=JSON.parse(l);if(e.type==='assistant'&&e.message?.usage)lu=e.message.usage}catch{}});await new Promise(res=>{r.on('close',()=>{if(lu)t=(lu.input_tokens||0)+(lu.output_tokens||0)+(lu.cache_creation_input_tokens||0)+(lu.cache_read_input_tokens||0);res()})});break}}}}const getContextLimit=m=>{if(m.includes('sonnet-4'))return 500000;if(m.includes('3-5-sonnet')||m.includes('sonnet-3.5'))return 200000;if(m.includes('haiku'))return 200000;if(m.includes('opus'))return 200000;return 200000};const cl=getContextLimit(m);const p=Math.min(100,Math.round(t/cl*100));const td=t>=1000000?(t/1000000).toFixed(1)+'M':t>=1000?(t/1000).toFixed(1)+'K':t.toString();const ld=cl>=1000000?(cl/1000000).toFixed(0)+'M':cl>=1000?(cl/1000).toFixed(0)+'K':cl.toString();const c=p>=90?'\\x1b[31m':p>=70?'\\x1b[33m':'\\x1b[32m';console.log('\\x1b[34m'+process.env.USER+'@'+require('os').hostname().split('.')[0]+'\\x1b[0m:\\x1b[34m'+dir+'\\x1b[0m ['+m+'] ๐Ÿช™ '+td+'/'+ld+' '+c+p+'%\\x1b[0m '+new Date().toLocaleTimeString('en-US',{hour12:false}))}catch{console.log('\\x1b[34m'+process.env.USER+'@'+require('os').hostname().split('.')[0]+'\\x1b[0m:\\x1b[34m'+(process.cwd().replace(process.env.HOME,'~'))+'\\x1b[0m [claude-sonnet-4] ๐Ÿช™ 0 \\x1b[31mโŒ\\x1b[0m '+new Date().toLocaleTimeString('en-US',{hour12:false}))}});\"" }, "model": "sonnet", - "dangerouslySkipPermissions": true + "disableBypassPermissionsMode": "enable" } From 6dae014cb89668a79abc72f08443954b999b97f9 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 12 Dec 2025 10:54:26 +0900 Subject: [PATCH 14/41] fix(settings): add defaultMode to bypassPermissions in model configuration --- config/claude/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/config/claude/settings.json b/config/claude/settings.json index 64d82ac00..57ac061a1 100644 --- a/config/claude/settings.json +++ b/config/claude/settings.json @@ -222,5 +222,6 @@ "command": "node -e \"const fs=require('fs'),path=require('path'),rl=require('readline');let i='';process.stdin.on('data',c=>i+=c);process.stdin.on('end',async()=>{try{const d=JSON.parse(i);const m=d.model?.display_name||'claude-sonnet-4';const dir=d.workspace?.current_dir?.replace(process.env.HOME,'~')||path.basename(process.cwd());const sid=d.session_id;let t=0;if(sid){const pdir=path.join(process.env.HOME,'.claude','projects');if(fs.existsSync(pdir)){for(const pd of fs.readdirSync(pdir).map(d=>path.join(pdir,d)).filter(d=>fs.statSync(d).isDirectory())){const tf=path.join(pd,sid+'.jsonl');if(fs.existsSync(tf)){const rs=fs.createReadStream(tf);const r=rl.createInterface({input:rs});let lu=null;r.on('line',l=>{try{const e=JSON.parse(l);if(e.type==='assistant'&&e.message?.usage)lu=e.message.usage}catch{}});await new Promise(res=>{r.on('close',()=>{if(lu)t=(lu.input_tokens||0)+(lu.output_tokens||0)+(lu.cache_creation_input_tokens||0)+(lu.cache_read_input_tokens||0);res()})});break}}}}const getContextLimit=m=>{if(m.includes('sonnet-4'))return 500000;if(m.includes('3-5-sonnet')||m.includes('sonnet-3.5'))return 200000;if(m.includes('haiku'))return 200000;if(m.includes('opus'))return 200000;return 200000};const cl=getContextLimit(m);const p=Math.min(100,Math.round(t/cl*100));const td=t>=1000000?(t/1000000).toFixed(1)+'M':t>=1000?(t/1000).toFixed(1)+'K':t.toString();const ld=cl>=1000000?(cl/1000000).toFixed(0)+'M':cl>=1000?(cl/1000).toFixed(0)+'K':cl.toString();const c=p>=90?'\\x1b[31m':p>=70?'\\x1b[33m':'\\x1b[32m';console.log('\\x1b[34m'+process.env.USER+'@'+require('os').hostname().split('.')[0]+'\\x1b[0m:\\x1b[34m'+dir+'\\x1b[0m ['+m+'] ๐Ÿช™ '+td+'/'+ld+' '+c+p+'%\\x1b[0m '+new Date().toLocaleTimeString('en-US',{hour12:false}))}catch{console.log('\\x1b[34m'+process.env.USER+'@'+require('os').hostname().split('.')[0]+'\\x1b[0m:\\x1b[34m'+(process.cwd().replace(process.env.HOME,'~'))+'\\x1b[0m [claude-sonnet-4] ๐Ÿช™ 0 \\x1b[31mโŒ\\x1b[0m '+new Date().toLocaleTimeString('en-US',{hour12:false}))}});\"" }, "model": "sonnet", + "defaultMode": "bypassPermissions", "disableBypassPermissionsMode": "enable" } From 35165984de0013d83d6994703316fdc84e8cb287 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 12 Dec 2025 10:57:31 +0900 Subject: [PATCH 15/41] fix(settings): remove disableBypassPermissionsMode and clean up settings.json --- config/claude/settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/claude/settings.json b/config/claude/settings.json index 57ac061a1..0bc9dd870 100644 --- a/config/claude/settings.json +++ b/config/claude/settings.json @@ -1,4 +1,5 @@ { + "cleanupPeriodDays": 0, "permissions": { "allow": [ "Bash(bun:*)", @@ -222,6 +223,5 @@ "command": "node -e \"const fs=require('fs'),path=require('path'),rl=require('readline');let i='';process.stdin.on('data',c=>i+=c);process.stdin.on('end',async()=>{try{const d=JSON.parse(i);const m=d.model?.display_name||'claude-sonnet-4';const dir=d.workspace?.current_dir?.replace(process.env.HOME,'~')||path.basename(process.cwd());const sid=d.session_id;let t=0;if(sid){const pdir=path.join(process.env.HOME,'.claude','projects');if(fs.existsSync(pdir)){for(const pd of fs.readdirSync(pdir).map(d=>path.join(pdir,d)).filter(d=>fs.statSync(d).isDirectory())){const tf=path.join(pd,sid+'.jsonl');if(fs.existsSync(tf)){const rs=fs.createReadStream(tf);const r=rl.createInterface({input:rs});let lu=null;r.on('line',l=>{try{const e=JSON.parse(l);if(e.type==='assistant'&&e.message?.usage)lu=e.message.usage}catch{}});await new Promise(res=>{r.on('close',()=>{if(lu)t=(lu.input_tokens||0)+(lu.output_tokens||0)+(lu.cache_creation_input_tokens||0)+(lu.cache_read_input_tokens||0);res()})});break}}}}const getContextLimit=m=>{if(m.includes('sonnet-4'))return 500000;if(m.includes('3-5-sonnet')||m.includes('sonnet-3.5'))return 200000;if(m.includes('haiku'))return 200000;if(m.includes('opus'))return 200000;return 200000};const cl=getContextLimit(m);const p=Math.min(100,Math.round(t/cl*100));const td=t>=1000000?(t/1000000).toFixed(1)+'M':t>=1000?(t/1000).toFixed(1)+'K':t.toString();const ld=cl>=1000000?(cl/1000000).toFixed(0)+'M':cl>=1000?(cl/1000).toFixed(0)+'K':cl.toString();const c=p>=90?'\\x1b[31m':p>=70?'\\x1b[33m':'\\x1b[32m';console.log('\\x1b[34m'+process.env.USER+'@'+require('os').hostname().split('.')[0]+'\\x1b[0m:\\x1b[34m'+dir+'\\x1b[0m ['+m+'] ๐Ÿช™ '+td+'/'+ld+' '+c+p+'%\\x1b[0m '+new Date().toLocaleTimeString('en-US',{hour12:false}))}catch{console.log('\\x1b[34m'+process.env.USER+'@'+require('os').hostname().split('.')[0]+'\\x1b[0m:\\x1b[34m'+(process.cwd().replace(process.env.HOME,'~'))+'\\x1b[0m [claude-sonnet-4] ๐Ÿช™ 0 \\x1b[31mโŒ\\x1b[0m '+new Date().toLocaleTimeString('en-US',{hour12:false}))}});\"" }, "model": "sonnet", - "defaultMode": "bypassPermissions", - "disableBypassPermissionsMode": "enable" + "defaultMode": "bypassPermissions" } From a0048b04dbbeb77b8466c5d7c19960e641b86aea Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 12 Dec 2025 10:57:48 +0900 Subject: [PATCH 16/41] fix(settings): update cleanupPeriodDays to 300 in settings.json --- config/claude/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/claude/settings.json b/config/claude/settings.json index 0bc9dd870..3eec963e2 100644 --- a/config/claude/settings.json +++ b/config/claude/settings.json @@ -1,5 +1,5 @@ { - "cleanupPeriodDays": 0, + "cleanupPeriodDays": 300, "permissions": { "allow": [ "Bash(bun:*)", From 3b6032c9439c4ac806bd73ae1aea787715bb70a8 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 12 Dec 2025 10:58:33 +0900 Subject: [PATCH 17/41] fix(settings): revert cleanupPeriodDays to -1 in settings.json --- config/claude/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/claude/settings.json b/config/claude/settings.json index 3eec963e2..8b12074b8 100644 --- a/config/claude/settings.json +++ b/config/claude/settings.json @@ -1,5 +1,5 @@ { - "cleanupPeriodDays": 300, + "cleanupPeriodDays": -1, "permissions": { "allow": [ "Bash(bun:*)", From 26a79e7952f2272f8d2ec03baa578be521c6b207 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 12 Dec 2025 10:59:34 +0900 Subject: [PATCH 18/41] fix(settings): update cleanupPeriodDays to 99999 in settings.json --- config/claude/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/claude/settings.json b/config/claude/settings.json index 8b12074b8..72e554ca1 100644 --- a/config/claude/settings.json +++ b/config/claude/settings.json @@ -1,5 +1,5 @@ { - "cleanupPeriodDays": -1, + "cleanupPeriodDays": 99999, "permissions": { "allow": [ "Bash(bun:*)", From 030c48dc2c157698820d1fbaab4b2a1b7531659e Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 12 Dec 2025 11:07:22 +0900 Subject: [PATCH 19/41] feat(security): add security.sh script to block dangerous Bash commands --- config/claude/default.nix | 5 +++ config/claude/security.sh | 72 +++++++++++++++++++++++++++++++++++++ config/claude/settings.json | 5 +++ 3 files changed, 82 insertions(+) create mode 100644 config/claude/security.sh diff --git a/config/claude/default.nix b/config/claude/default.nix index 0a439a7b5..ce3bc5d34 100644 --- a/config/claude/default.nix +++ b/config/claude/default.nix @@ -13,4 +13,9 @@ source = ./notify.sh; executable = true; }; + + home.file.".claude/security.sh" = { + source = ./security.sh; + executable = true; + }; } diff --git a/config/claude/security.sh b/config/claude/security.sh new file mode 100644 index 000000000..3b5133d82 --- /dev/null +++ b/config/claude/security.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash + +# Claude Code Security Hook +# Blocks dangerous Bash commands by checking against deny patterns +# Based on: https://wasabeef.jp/blog/claude-code-secure-bash +# +# This script runs as a PreToolUse hook and returns: +# - Exit code 0: Command is allowed +# - Exit code 2: Command is blocked + +set -euo pipefail + +# Read JSON input from stdin +input=$(cat) + +# Extract tool name - only process Bash commands +tool_name=$(echo "$input" | jq -r '.tool.name // empty' 2>/dev/null) +[[ "$tool_name" != "Bash" ]] && exit 0 + +# Extract the command to be executed +command=$(echo "$input" | jq -r '.tool.input.command // empty' 2>/dev/null) +[[ -z "$command" ]] && exit 0 + +# Path to settings file with deny patterns +settings="$HOME/.claude/settings.json" +[[ ! -f "$settings" ]] && exit 0 + +# Read deny patterns from settings +mapfile -t deny_patterns < <(jq -r '.permissions.deny[]?' "$settings" 2>/dev/null) + +# Function to check if a command matches a pattern +# Supports glob-style wildcards +matches_pattern() { + local cmd="$1" + local pattern="$2" + + # Extract pattern from Bash(...) format + if [[ "$pattern" =~ ^Bash\((.+)\)$ ]]; then + local check_pattern="${BASH_REMATCH[1]}" + # Remove trailing :* if present (legacy format) + check_pattern="${check_pattern%:*}" + + # Use bash glob matching (extended globbing) + shopt -s extglob + # shellcheck disable=SC2053 + if [[ "$cmd" == $check_pattern ]]; then + return 0 + fi + shopt -u extglob + fi + return 1 +} + +# Split command at logical operators to catch hidden dangerous commands +# This handles: cmd1 ; cmd2, cmd1 && cmd2, cmd1 || cmd2, cmd1 | cmd2 +IFS=$'\n' read -r -d '' -a segments < <(echo "$command" | sed 's/[;&|]\+/\n/g' && printf '\0') || true + +for segment in "${segments[@]}"; do + # Trim leading/trailing whitespace + segment=$(echo "$segment" | xargs 2>/dev/null) || continue + [[ -z "$segment" ]] && continue + + for pattern in "${deny_patterns[@]}"; do + if matches_pattern "$segment" "$pattern"; then + echo "BLOCKED by security.sh: Command '$segment' matches deny pattern '$pattern'" >&2 + exit 2 + fi + done +done + +# Command passed all checks +exit 0 diff --git a/config/claude/settings.json b/config/claude/settings.json index 72e554ca1..b2cc40220 100644 --- a/config/claude/settings.json +++ b/config/claude/settings.json @@ -124,6 +124,11 @@ { "matcher": "Bash", "hooks": [ + { + "type": "command", + "command": "$HOME/.claude/security.sh", + "timeout": 5 + }, { "type": "command", "command": "$HOME/.claude/notify.sh", From 96d36ecca5ed5596afe434d6eeb806dbe8ecc9e8 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 05:48:55 +0900 Subject: [PATCH 20/41] fix(pushover): skip notifications for "other" session end reason --- config/claude/pushover.sh | 4 ++++ flake.lock | 12 ++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/config/claude/pushover.sh b/config/claude/pushover.sh index 9a2732882..645bbb421 100755 --- a/config/claude/pushover.sh +++ b/config/claude/pushover.sh @@ -112,8 +112,12 @@ if echo "$input" | jq -e '.message' >/dev/null 2>&1; then fi # Handle SessionEnd hook (priority 0 = normal) +# Skip "other" reason - it's a generic/unknown reason that's noisy if echo "$input" | jq -e '.reason' >/dev/null 2>&1; then REASON=$(echo "$input" | jq -r '.reason') + if [ "$REASON" = "other" ]; then + exit 0 + fi send_notification "๐Ÿ‘‹ Session ended: ${REASON}" 0 exit 0 fi diff --git a/flake.lock b/flake.lock index 313618d6c..850e5a943 100644 --- a/flake.lock +++ b/flake.lock @@ -405,11 +405,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1765270179, - "narHash": "sha256-g2a4MhRKu4ymR4xwo+I+auTknXt/+j37Lnf0Mvfl1rE=", + "lastModified": 1765425892, + "narHash": "sha256-jlQpSkg2sK6IJVzTQBDyRxQZgKADC2HKMRfGCSgNMHo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "677fbe97984e7af3175b6c121f3c39ee5c8d62c9", + "rev": "5d6bdbddb4695a62f0d00a3620b37a15275a5093", "type": "github" }, "original": { @@ -442,11 +442,11 @@ ] }, "locked": { - "lastModified": 1765496834, - "narHash": "sha256-B8AnKiB4U1nW69aGsuLICwIQ3Qid8/ok9HZQAihBaHo=", + "lastModified": 1765567938, + "narHash": "sha256-cT4D+Q1n1zKEfur3z6x01fUhB8IlvfG9V8WBaJqzhZ0=", "owner": "nix-community", "repo": "NUR", - "rev": "61bb240f28350baef6922c9d77405296e50440a7", + "rev": "5414593b5c7fb86679b0f10184ecde9e5849275a", "type": "github" }, "original": { From d19f04169a6953c36ba37fdd4c57bd31f71874e6 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 05:56:07 +0900 Subject: [PATCH 21/41] feat(tests): add ShellSpec tests and integrate into CI workflow --- .github/workflows/shell.yml | 47 ++++++++++++++++ .shellspec | 3 ++ Makefile | 9 +++- home-manager/packages/default.nix | 1 + spec/notify_spec.sh | 50 +++++++++++++++++ spec/pushover_spec.sh | 56 +++++++++++++++++++ spec/security_spec.sh | 89 +++++++++++++++++++++++++++++++ spec/spec_helper.sh | 11 ++++ spec/support/custom_matcher.sh | 1 + 9 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/shell.yml create mode 100644 .shellspec create mode 100644 spec/notify_spec.sh create mode 100644 spec/pushover_spec.sh create mode 100644 spec/security_spec.sh create mode 100644 spec/spec_helper.sh create mode 100644 spec/support/custom_matcher.sh diff --git a/.github/workflows/shell.yml b/.github/workflows/shell.yml new file mode 100644 index 000000000..3e350d0d0 --- /dev/null +++ b/.github/workflows/shell.yml @@ -0,0 +1,47 @@ +name: Shell Tests + +on: + push: + branches: [main] + paths: + - "**/*.sh" + - "spec/**" + - ".shellspec" + - ".github/workflows/shell.yml" + pull_request: + branches: [main] + paths: + - "**/*.sh" + - "spec/**" + - ".shellspec" + - ".github/workflows/shell.yml" + +jobs: + shellspec: + name: ShellSpec Tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install ShellSpec + run: | + curl -fsSL https://git.io/shellspec | sh -s -- --yes + + - name: Run ShellSpec + run: | + export PATH="$HOME/.local/lib/shellspec:$PATH" + shellspec + + shellcheck: + name: ShellCheck + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run ShellCheck + uses: ludeeus/action-shellcheck@master + with: + scandir: "./config/claude" + ignore_paths: "node_modules" diff --git a/.shellspec b/.shellspec new file mode 100644 index 000000000..909b8d05c --- /dev/null +++ b/.shellspec @@ -0,0 +1,3 @@ +--require spec_helper +--format progress +--shell bash diff --git a/Makefile b/Makefile index 79f75d9b5..24273b38a 100644 --- a/Makefile +++ b/Makefile @@ -138,7 +138,7 @@ setup-dev: nix-setup git-submodule-sync shell-install ## Set up local developmen switch: nix-switch launchctl ## Apply Nix configuration and restart launchd agents. .PHONY: test -test: neovim-test +test: neovim-test shell-test ## Run all tests (neovim + shell). .PHONY: update update: nix-update shell-update neovim-update ## Update Nix flake and configurations. @@ -624,3 +624,10 @@ git-submodule-sync: ## Sync and update git submodules. @git submodule sync @git submodule update --init --recursive @echo "โœ… Submodules synced and updated" + +##@ Shell + +.PHONY: shell-test +shell-test: ## Run shell script tests using ShellSpec. + @echo "๐Ÿงช Running shell tests..." + @shellspec diff --git a/home-manager/packages/default.nix b/home-manager/packages/default.nix index ffe8a1fca..4a6246eaf 100644 --- a/home-manager/packages/default.nix +++ b/home-manager/packages/default.nix @@ -61,6 +61,7 @@ with pkgs; ripgrep rustup sccache + shellspec speedtest-cli sqlite stern diff --git a/spec/notify_spec.sh b/spec/notify_spec.sh new file mode 100644 index 000000000..4a1c33fe9 --- /dev/null +++ b/spec/notify_spec.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +Describe 'notify.sh' + SCRIPT="$PWD/config/claude/notify.sh" + + Describe 'when Pushover is configured' + setup() { + export PUSHOVER_API_TOKEN="test_token" + export PUSHOVER_USER_KEY="test_user" + } + Before 'setup' + + It 'exits early and skips local notification' + When run bash "$SCRIPT" <<< '{"message": "Test message"}' + The status should be success + The output should eq '' + End + End + + Describe 'Notification hook (no Pushover)' + setup() { + unset PUSHOVER_API_TOKEN 2>/dev/null || true + unset PUSHOVER_USER_KEY 2>/dev/null || true + } + Before 'setup' + + It 'exits 0 for login notification' + When run bash "$SCRIPT" <<< '{"message": "Claude Code login successful"}' + The status should be success + End + + It 'exits 0 for waiting notification' + When run bash "$SCRIPT" <<< '{"message": "Claude is waiting for your input"}' + The status should be success + End + End + + Describe 'SessionEnd hook (no Pushover)' + setup() { + unset PUSHOVER_API_TOKEN 2>/dev/null || true + unset PUSHOVER_USER_KEY 2>/dev/null || true + } + Before 'setup' + + It 'exits 0 for session end' + When run bash "$SCRIPT" <<< '{"reason": "user_exit"}' + The status should be success + End + End +End diff --git a/spec/pushover_spec.sh b/spec/pushover_spec.sh new file mode 100644 index 000000000..3ae1d8d17 --- /dev/null +++ b/spec/pushover_spec.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +Describe 'pushover.sh' + SCRIPT="$PWD/config/claude/pushover.sh" + + Describe 'credential handling' + It 'exits 0 when no credentials are set' + unset PUSHOVER_API_TOKEN 2>/dev/null || true + unset PUSHOVER_USER_KEY 2>/dev/null || true + When run bash "$SCRIPT" <<< '{}' + The status should be success + The output should eq '' + End + End + + Describe 'SessionEnd hook' + setup() { + export PUSHOVER_API_TOKEN="test_token" + export PUSHOVER_USER_KEY="test_user" + } + Before 'setup' + + mock_curl() { + echo "CURL_CALLED" + } + + It 'skips notification for "other" reason' + When run bash "$SCRIPT" <<< '{"reason": "other"}' + The status should be success + The output should eq '' + End + + It 'processes notification for "user_exit" reason' + When run bash "$SCRIPT" <<< '{"reason": "user_exit"}' + The status should be success + End + End + + Describe 'Notification hook' + setup() { + export PUSHOVER_API_TOKEN="test_token" + export PUSHOVER_USER_KEY="test_user" + } + Before 'setup' + + It 'skips login notification' + When run bash "$SCRIPT" <<< '{"message": "Claude Code login successful"}' + The status should be success + End + + It 'processes waiting notification' + When run bash "$SCRIPT" <<< '{"message": "Claude is waiting for your input"}' + The status should be success + End + End +End diff --git a/spec/security_spec.sh b/spec/security_spec.sh new file mode 100644 index 000000000..7d456ea8c --- /dev/null +++ b/spec/security_spec.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash + +Describe 'security.sh' + SCRIPT="$PWD/config/claude/security.sh" + + setup() { + TEMP_HOME=$(mktemp -d) + ORIGINAL_HOME="$HOME" + mkdir -p "$TEMP_HOME/.claude" + cat > "$TEMP_HOME/.claude/settings.json" << 'SETTINGS' +{ + "permissions": { + "deny": [ + "Bash(sudo:*)", + "Bash(rm -rf /*:*)", + "Bash(rm -rf ~/*:*)", + "Bash(chmod -R 777:*)", + "Bash(mkfs:*)", + "Bash(dd if=:*)" + ] + } +} +SETTINGS + } + + cleanup() { + rm -rf "$TEMP_HOME" + } + + Before 'setup' + After 'cleanup' + + Describe 'tool filtering' + It 'passes non-Bash tools through' + Data '{"tool": {"name": "Read", "input": {}}}' + When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" + The status should be success + End + + It 'passes when no tool specified' + Data '{}' + When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" + The status should be success + End + End + + Describe 'safe commands' + It 'allows ls -la' + Data '{"tool": {"name": "Bash", "input": {"command": "ls -la"}}}' + When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" + The status should be success + End + + It 'allows git status' + Data '{"tool": {"name": "Bash", "input": {"command": "git status"}}}' + When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" + The status should be success + End + + It 'allows cat /etc/hosts' + Data '{"tool": {"name": "Bash", "input": {"command": "cat /etc/hosts"}}}' + When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" + The status should be success + End + + It 'allows echo hello' + Data '{"tool": {"name": "Bash", "input": {"command": "echo hello"}}}' + When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" + The status should be success + End + End + + Describe 'blocked commands' + It 'blocks rm -rf /*' + Data '{"tool": {"name": "Bash", "input": {"command": "rm -rf /*"}}}' + When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" + The status should eq 2 + The stderr should include 'BLOCKED' + End + End + + Describe 'edge cases' + It 'passes with empty input' + Data '{"tool": {"name": "Bash", "input": {}}}' + When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" + The status should be success + End + End +End diff --git a/spec/spec_helper.sh b/spec/spec_helper.sh new file mode 100644 index 000000000..23bd7bad7 --- /dev/null +++ b/spec/spec_helper.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -eu + +spec_helper_precheck() { + minimum_version "0.28.1" +} + +spec_helper_configure() { + import 'support/custom_matcher' +} diff --git a/spec/support/custom_matcher.sh b/spec/support/custom_matcher.sh new file mode 100644 index 000000000..f1f641af1 --- /dev/null +++ b/spec/support/custom_matcher.sh @@ -0,0 +1 @@ +#!/usr/bin/env bash From 3e56c76c77fae8bb2e87b39bd64ab3b85c5d9aea Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 05:58:52 +0900 Subject: [PATCH 22/41] feat(ci): streamline Shell workflow and add Nix dev shell testing --- .github/workflows/shell.yml | 60 +++++++++++++++---------------------- Makefile | 5 ++++ 2 files changed, 29 insertions(+), 36 deletions(-) diff --git a/.github/workflows/shell.yml b/.github/workflows/shell.yml index 3e350d0d0..abc813d01 100644 --- a/.github/workflows/shell.yml +++ b/.github/workflows/shell.yml @@ -1,47 +1,35 @@ -name: Shell Tests - +name: Shell on: push: - branches: [main] - paths: - - "**/*.sh" - - "spec/**" - - ".shellspec" - - ".github/workflows/shell.yml" + branches: + - main pull_request: - branches: [main] - paths: - - "**/*.sh" - - "spec/**" - - ".shellspec" - - ".github/workflows/shell.yml" - + branches: + - main +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.sha }} + cancel-in-progress: true jobs: shellspec: - name: ShellSpec Tests runs-on: ubuntu-latest + timeout-minutes: 30 steps: - name: Checkout - uses: actions/checkout@v4 - - - name: Install ShellSpec - run: | - curl -fsSL https://git.io/shellspec | sh -s -- --yes - - - name: Run ShellSpec - run: | - export PATH="$HOME/.local/lib/shellspec:$PATH" - shellspec - - shellcheck: - name: ShellCheck + uses: actions/checkout@v6 + - name: Install Nix + uses: cachix/install-nix-action@v31 + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + - name: Run ShellSpec Tests (Dev Shell) + run: make shell-test-dev + shell-check: + if: always() + needs: + - shellspec runs-on: ubuntu-latest + timeout-minutes: 3 steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Run ShellCheck - uses: ludeeus/action-shellcheck@master + - name: Alls Green + uses: re-actors/alls-green@release/v1 with: - scandir: "./config/claude" - ignore_paths: "node_modules" + jobs: ${{ toJSON(needs) }} diff --git a/Makefile b/Makefile index 24273b38a..704331883 100644 --- a/Makefile +++ b/Makefile @@ -631,3 +631,8 @@ git-submodule-sync: ## Sync and update git submodules. shell-test: ## Run shell script tests using ShellSpec. @echo "๐Ÿงช Running shell tests..." @shellspec + +.PHONY: shell-test-dev +shell-test-dev: ## Run shell tests inside the Nix dev shell (mirrors CI). + @echo "๐Ÿงช Running shell tests inside the Nix dev shell..." + @DEVENV_ROOT=$(CURDIR) $(NIX_ALLOW_UNFREE) $(NIX_EXEC) develop $(NIX_FLAGS) .# --command $(MAKE) shell-test From 8e1bbe2082ee146723dc247007ea43ebf2a34ae0 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 05:59:31 +0900 Subject: [PATCH 23/41] feat(devenv): add ShellSpec to development environment packages --- devenv.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/devenv.nix b/devenv.nix index 2dc95190e..e8bd155fa 100644 --- a/devenv.nix +++ b/devenv.nix @@ -5,6 +5,7 @@ pkgs.nodejs pkgs.bun pkgs.neovim + pkgs.shellspec ]; containers = pkgs.lib.mkIf (!pkgs.stdenv.hostPlatform.isLinux) (pkgs.lib.mkForce { }); From 19fd5e6d37563bb56eaf083df35b7408bf9ae07b Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 06:01:47 +0900 Subject: [PATCH 24/41] feat(ci): add ShellCheck to CI workflow and development environment --- .github/workflows/shell.yml | 13 +++++++++++++ Makefile | 10 ++++++++++ devenv.nix | 1 + 3 files changed, 24 insertions(+) diff --git a/.github/workflows/shell.yml b/.github/workflows/shell.yml index abc813d01..258232121 100644 --- a/.github/workflows/shell.yml +++ b/.github/workflows/shell.yml @@ -22,10 +22,23 @@ jobs: github_access_token: ${{ secrets.GITHUB_TOKEN }} - name: Run ShellSpec Tests (Dev Shell) run: make shell-test-dev + shellcheck: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Install Nix + uses: cachix/install-nix-action@v31 + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + - name: Run ShellCheck (Dev Shell) + run: make shell-check-dev shell-check: if: always() needs: - shellspec + - shellcheck runs-on: ubuntu-latest timeout-minutes: 3 steps: diff --git a/Makefile b/Makefile index 704331883..2198b74a8 100644 --- a/Makefile +++ b/Makefile @@ -636,3 +636,13 @@ shell-test: ## Run shell script tests using ShellSpec. shell-test-dev: ## Run shell tests inside the Nix dev shell (mirrors CI). @echo "๐Ÿงช Running shell tests inside the Nix dev shell..." @DEVENV_ROOT=$(CURDIR) $(NIX_ALLOW_UNFREE) $(NIX_EXEC) develop $(NIX_FLAGS) .# --command $(MAKE) shell-test + +.PHONY: shell-check +shell-check: ## Run ShellCheck on shell scripts. + @echo "๐Ÿ” Running ShellCheck..." + @find . -name '*.sh' -not -path './node_modules/*' -not -path './.git/*' -not -path './result/*' | xargs shellcheck + +.PHONY: shell-check-dev +shell-check-dev: ## Run ShellCheck inside the Nix dev shell (mirrors CI). + @echo "๐Ÿ” Running ShellCheck inside the Nix dev shell..." + @DEVENV_ROOT=$(CURDIR) $(NIX_ALLOW_UNFREE) $(NIX_EXEC) develop $(NIX_FLAGS) .# --command $(MAKE) shell-check diff --git a/devenv.nix b/devenv.nix index e8bd155fa..a6a786769 100644 --- a/devenv.nix +++ b/devenv.nix @@ -5,6 +5,7 @@ pkgs.nodejs pkgs.bun pkgs.neovim + pkgs.shellcheck pkgs.shellspec ]; From d2132c38073138c7d322c13ea33bf3a9b7b48f38 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 06:02:30 +0900 Subject: [PATCH 25/41] feat(scripts): add shellcheck directives to notify and install scripts --- config/claude/notify.sh | 1 + install.sh | 4 +++- spec/security_spec.sh | 1 - 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config/claude/notify.sh b/config/claude/notify.sh index 1bf8769b6..5f9be4d18 100644 --- a/config/claude/notify.sh +++ b/config/claude/notify.sh @@ -8,6 +8,7 @@ if [ -z "$PUSHOVER_API_TOKEN" ] || [ -z "$PUSHOVER_USER_KEY" ]; then if [ -f "$HOME/dotfiles/.env" ]; then set -a + # shellcheck source=/dev/null source "$HOME/dotfiles/.env" 2>/dev/null set +a fi diff --git a/install.sh b/install.sh index 429d8f996..7318f5f16 100755 --- a/install.sh +++ b/install.sh @@ -44,6 +44,7 @@ if ! command -v nix >/dev/null 2>&1; then if [ "$OS" = "macos" ]; then curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install # For macOS, source the Nix profile immediately to update PATH in CI. + # shellcheck source=/dev/null . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh NIX_EFFECTIVE_BIN_PATH="/nix/var/nix/profiles/default/bin" else # Linux @@ -52,6 +53,7 @@ if ! command -v nix >/dev/null 2>&1; then curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install linux --init none --no-confirm # Source the Nix profile script to add Nix to PATH for the current shell if [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then + # shellcheck source=/dev/null . "$HOME/.nix-profile/etc/profile.d/nix.sh" echo "Sourced Nix profile for Determinate Nix (Docker) setup." else @@ -64,7 +66,7 @@ if ! command -v nix >/dev/null 2>&1; then echo "Performing Determinate Nix multi-user installation..." curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install linux # For Linux multi-user installations, add the default Nix path for the current shell. - export PATH=/nix/var/nix/profiles/default/bin:$PATH + export PATH="/nix/var/nix/profiles/default/bin:$PATH" NIX_EFFECTIVE_BIN_PATH="/nix/var/nix/profiles/default/bin" fi fi diff --git a/spec/security_spec.sh b/spec/security_spec.sh index 7d456ea8c..0d112a270 100644 --- a/spec/security_spec.sh +++ b/spec/security_spec.sh @@ -5,7 +5,6 @@ Describe 'security.sh' setup() { TEMP_HOME=$(mktemp -d) - ORIGINAL_HOME="$HOME" mkdir -p "$TEMP_HOME/.claude" cat > "$TEMP_HOME/.claude/settings.json" << 'SETTINGS' { From 5b89d01a553926d20bd64d44b8589f1a41d07537 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 06:02:45 +0900 Subject: [PATCH 26/41] feat(tests): add ShellCheck directive to pushover_spec.sh --- home-manager/packages/default.nix | 1 + spec/notify_spec.sh | 1 + spec/pushover_spec.sh | 1 + 3 files changed, 3 insertions(+) diff --git a/home-manager/packages/default.nix b/home-manager/packages/default.nix index 4a6246eaf..6da10a6f4 100644 --- a/home-manager/packages/default.nix +++ b/home-manager/packages/default.nix @@ -61,6 +61,7 @@ with pkgs; ripgrep rustup sccache + shellcheck shellspec speedtest-cli sqlite diff --git a/spec/notify_spec.sh b/spec/notify_spec.sh index 4a1c33fe9..ac70134a7 100644 --- a/spec/notify_spec.sh +++ b/spec/notify_spec.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=SC2329 Describe 'notify.sh' SCRIPT="$PWD/config/claude/notify.sh" diff --git a/spec/pushover_spec.sh b/spec/pushover_spec.sh index 3ae1d8d17..c1ba9412a 100644 --- a/spec/pushover_spec.sh +++ b/spec/pushover_spec.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=SC2329 Describe 'pushover.sh' SCRIPT="$PWD/config/claude/pushover.sh" From e4851ed6a6e376a29175b0b2e7496ed5c46503ac Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 06:03:13 +0900 Subject: [PATCH 27/41] feat(tests): add ShellCheck directive to security_spec.sh --- config/claude/pushover.sh | 2 +- spec/security_spec.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/config/claude/pushover.sh b/config/claude/pushover.sh index 645bbb421..65f4b33e8 100755 --- a/config/claude/pushover.sh +++ b/config/claude/pushover.sh @@ -9,8 +9,8 @@ # inherit shell environment variables if [ -z "$PUSHOVER_API_TOKEN" ] || [ -z "$PUSHOVER_USER_KEY" ]; then if [ -f "$HOME/dotfiles/.env" ]; then - # shellcheck source=/dev/null set -a + # shellcheck source=/dev/null source "$HOME/dotfiles/.env" 2>/dev/null set +a fi diff --git a/spec/security_spec.sh b/spec/security_spec.sh index 0d112a270..d15dd61c6 100644 --- a/spec/security_spec.sh +++ b/spec/security_spec.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=SC2329 Describe 'security.sh' SCRIPT="$PWD/config/claude/security.sh" From 70814c9c47e143adaed64d43b0a976b7d733eba4 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 06:04:57 +0900 Subject: [PATCH 28/41] feat(scripts): add ShellCheck directive to security.sh and sync.sh --- config/claude/security.sh | 1 + home-manager/services/code-syncer/sync.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/config/claude/security.sh b/config/claude/security.sh index 3b5133d82..ab6ef9408 100644 --- a/config/claude/security.sh +++ b/config/claude/security.sh @@ -53,6 +53,7 @@ matches_pattern() { # Split command at logical operators to catch hidden dangerous commands # This handles: cmd1 ; cmd2, cmd1 && cmd2, cmd1 || cmd2, cmd1 | cmd2 +# shellcheck disable=SC2001 IFS=$'\n' read -r -d '' -a segments < <(echo "$command" | sed 's/[;&|]\+/\n/g' && printf '\0') || true for segment in "${segments[@]}"; do diff --git a/home-manager/services/code-syncer/sync.sh b/home-manager/services/code-syncer/sync.sh index 59651afe7..b65ec4d40 100755 --- a/home-manager/services/code-syncer/sync.sh +++ b/home-manager/services/code-syncer/sync.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=SC2034,SC2155,SC2181,SC2162 # --- CONFIGURATION --- From 1b42be55be49392b186b672cb143b03b0f52cdc3 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 06:11:31 +0900 Subject: [PATCH 29/41] feat(tests): refactor test scripts for improved readability and consistency --- config/claude/security.sh | 12 +-- home-manager/services/cliproxyapi/start.sh | 4 +- spec/notify_spec.sh | 92 ++++++++-------- spec/pushover_spec.sh | 104 +++++++++--------- spec/security_spec.sh | 120 ++++++++++----------- 5 files changed, 166 insertions(+), 166 deletions(-) diff --git a/config/claude/security.sh b/config/claude/security.sh index ab6ef9408..40bc3dddc 100644 --- a/config/claude/security.sh +++ b/config/claude/security.sh @@ -15,15 +15,15 @@ input=$(cat) # Extract tool name - only process Bash commands tool_name=$(echo "$input" | jq -r '.tool.name // empty' 2>/dev/null) -[[ "$tool_name" != "Bash" ]] && exit 0 +[[ $tool_name != "Bash" ]] && exit 0 # Extract the command to be executed command=$(echo "$input" | jq -r '.tool.input.command // empty' 2>/dev/null) -[[ -z "$command" ]] && exit 0 +[[ -z $command ]] && exit 0 # Path to settings file with deny patterns settings="$HOME/.claude/settings.json" -[[ ! -f "$settings" ]] && exit 0 +[[ ! -f $settings ]] && exit 0 # Read deny patterns from settings mapfile -t deny_patterns < <(jq -r '.permissions.deny[]?' "$settings" 2>/dev/null) @@ -35,7 +35,7 @@ matches_pattern() { local pattern="$2" # Extract pattern from Bash(...) format - if [[ "$pattern" =~ ^Bash\((.+)\)$ ]]; then + if [[ $pattern =~ ^Bash\((.+)\)$ ]]; then local check_pattern="${BASH_REMATCH[1]}" # Remove trailing :* if present (legacy format) check_pattern="${check_pattern%:*}" @@ -43,7 +43,7 @@ matches_pattern() { # Use bash glob matching (extended globbing) shopt -s extglob # shellcheck disable=SC2053 - if [[ "$cmd" == $check_pattern ]]; then + if [[ $cmd == $check_pattern ]]; then return 0 fi shopt -u extglob @@ -59,7 +59,7 @@ IFS=$'\n' read -r -d '' -a segments < <(echo "$command" | sed 's/[;&|]\+/\n/g' & for segment in "${segments[@]}"; do # Trim leading/trailing whitespace segment=$(echo "$segment" | xargs 2>/dev/null) || continue - [[ -z "$segment" ]] && continue + [[ -z $segment ]] && continue for pattern in "${deny_patterns[@]}"; do if matches_pattern "$segment" "$pattern"; then diff --git a/home-manager/services/cliproxyapi/start.sh b/home-manager/services/cliproxyapi/start.sh index 02fa72dad..70aecea80 100755 --- a/home-manager/services/cliproxyapi/start.sh +++ b/home-manager/services/cliproxyapi/start.sh @@ -21,8 +21,8 @@ export MANAGEMENT_PASSWORD="${CLIPROXY_MANAGEMENT_PASSWORD:-}" # Generate config from template with secrets injected if [ -f "$TEMPLATE" ]; then sed -e "s|__OPENROUTER_API_KEY__|${OPENROUTER_API_KEY:-}|g" \ - -e "s|__CLIPROXY_MANAGEMENT_PASSWORD__|${CLIPROXY_MANAGEMENT_PASSWORD:-}|g" \ - "$TEMPLATE" >"$CONFIG" + -e "s|__CLIPROXY_MANAGEMENT_PASSWORD__|${CLIPROXY_MANAGEMENT_PASSWORD:-}|g" \ + "$TEMPLATE" >"$CONFIG" fi # Change to config dir so logs are created there diff --git a/spec/notify_spec.sh b/spec/notify_spec.sh index ac70134a7..fd8883870 100644 --- a/spec/notify_spec.sh +++ b/spec/notify_spec.sh @@ -2,50 +2,50 @@ # shellcheck disable=SC2329 Describe 'notify.sh' - SCRIPT="$PWD/config/claude/notify.sh" - - Describe 'when Pushover is configured' - setup() { - export PUSHOVER_API_TOKEN="test_token" - export PUSHOVER_USER_KEY="test_user" - } - Before 'setup' - - It 'exits early and skips local notification' - When run bash "$SCRIPT" <<< '{"message": "Test message"}' - The status should be success - The output should eq '' - End - End - - Describe 'Notification hook (no Pushover)' - setup() { - unset PUSHOVER_API_TOKEN 2>/dev/null || true - unset PUSHOVER_USER_KEY 2>/dev/null || true - } - Before 'setup' - - It 'exits 0 for login notification' - When run bash "$SCRIPT" <<< '{"message": "Claude Code login successful"}' - The status should be success - End - - It 'exits 0 for waiting notification' - When run bash "$SCRIPT" <<< '{"message": "Claude is waiting for your input"}' - The status should be success - End - End - - Describe 'SessionEnd hook (no Pushover)' - setup() { - unset PUSHOVER_API_TOKEN 2>/dev/null || true - unset PUSHOVER_USER_KEY 2>/dev/null || true - } - Before 'setup' - - It 'exits 0 for session end' - When run bash "$SCRIPT" <<< '{"reason": "user_exit"}' - The status should be success - End - End +SCRIPT="$PWD/config/claude/notify.sh" + +Describe 'when Pushover is configured' +setup() { + export PUSHOVER_API_TOKEN="test_token" + export PUSHOVER_USER_KEY="test_user" +} +Before 'setup' + +It 'exits early and skips local notification' +When run bash "$SCRIPT" <<<'{"message": "Test message"}' +The status should be success +The output should eq '' +End +End + +Describe 'Notification hook (no Pushover)' +setup() { + unset PUSHOVER_API_TOKEN 2>/dev/null || true + unset PUSHOVER_USER_KEY 2>/dev/null || true +} +Before 'setup' + +It 'exits 0 for login notification' +When run bash "$SCRIPT" <<<'{"message": "Claude Code login successful"}' +The status should be success +End + +It 'exits 0 for waiting notification' +When run bash "$SCRIPT" <<<'{"message": "Claude is waiting for your input"}' +The status should be success +End +End + +Describe 'SessionEnd hook (no Pushover)' +setup() { + unset PUSHOVER_API_TOKEN 2>/dev/null || true + unset PUSHOVER_USER_KEY 2>/dev/null || true +} +Before 'setup' + +It 'exits 0 for session end' +When run bash "$SCRIPT" <<<'{"reason": "user_exit"}' +The status should be success +End +End End diff --git a/spec/pushover_spec.sh b/spec/pushover_spec.sh index c1ba9412a..25a845e06 100644 --- a/spec/pushover_spec.sh +++ b/spec/pushover_spec.sh @@ -2,56 +2,56 @@ # shellcheck disable=SC2329 Describe 'pushover.sh' - SCRIPT="$PWD/config/claude/pushover.sh" - - Describe 'credential handling' - It 'exits 0 when no credentials are set' - unset PUSHOVER_API_TOKEN 2>/dev/null || true - unset PUSHOVER_USER_KEY 2>/dev/null || true - When run bash "$SCRIPT" <<< '{}' - The status should be success - The output should eq '' - End - End - - Describe 'SessionEnd hook' - setup() { - export PUSHOVER_API_TOKEN="test_token" - export PUSHOVER_USER_KEY="test_user" - } - Before 'setup' - - mock_curl() { - echo "CURL_CALLED" - } - - It 'skips notification for "other" reason' - When run bash "$SCRIPT" <<< '{"reason": "other"}' - The status should be success - The output should eq '' - End - - It 'processes notification for "user_exit" reason' - When run bash "$SCRIPT" <<< '{"reason": "user_exit"}' - The status should be success - End - End - - Describe 'Notification hook' - setup() { - export PUSHOVER_API_TOKEN="test_token" - export PUSHOVER_USER_KEY="test_user" - } - Before 'setup' - - It 'skips login notification' - When run bash "$SCRIPT" <<< '{"message": "Claude Code login successful"}' - The status should be success - End - - It 'processes waiting notification' - When run bash "$SCRIPT" <<< '{"message": "Claude is waiting for your input"}' - The status should be success - End - End +SCRIPT="$PWD/config/claude/pushover.sh" + +Describe 'credential handling' +It 'exits 0 when no credentials are set' +unset PUSHOVER_API_TOKEN 2>/dev/null || true +unset PUSHOVER_USER_KEY 2>/dev/null || true +When run bash "$SCRIPT" <<<'{}' +The status should be success +The output should eq '' +End +End + +Describe 'SessionEnd hook' +setup() { + export PUSHOVER_API_TOKEN="test_token" + export PUSHOVER_USER_KEY="test_user" +} +Before 'setup' + +mock_curl() { + echo "CURL_CALLED" +} + +It 'skips notification for "other" reason' +When run bash "$SCRIPT" <<<'{"reason": "other"}' +The status should be success +The output should eq '' +End + +It 'processes notification for "user_exit" reason' +When run bash "$SCRIPT" <<<'{"reason": "user_exit"}' +The status should be success +End +End + +Describe 'Notification hook' +setup() { + export PUSHOVER_API_TOKEN="test_token" + export PUSHOVER_USER_KEY="test_user" +} +Before 'setup' + +It 'skips login notification' +When run bash "$SCRIPT" <<<'{"message": "Claude Code login successful"}' +The status should be success +End + +It 'processes waiting notification' +When run bash "$SCRIPT" <<<'{"message": "Claude is waiting for your input"}' +The status should be success +End +End End diff --git a/spec/security_spec.sh b/spec/security_spec.sh index d15dd61c6..9a3e547b9 100644 --- a/spec/security_spec.sh +++ b/spec/security_spec.sh @@ -2,12 +2,12 @@ # shellcheck disable=SC2329 Describe 'security.sh' - SCRIPT="$PWD/config/claude/security.sh" +SCRIPT="$PWD/config/claude/security.sh" - setup() { - TEMP_HOME=$(mktemp -d) - mkdir -p "$TEMP_HOME/.claude" - cat > "$TEMP_HOME/.claude/settings.json" << 'SETTINGS' +setup() { + TEMP_HOME=$(mktemp -d) + mkdir -p "$TEMP_HOME/.claude" + cat >"$TEMP_HOME/.claude/settings.json" <<'SETTINGS' { "permissions": { "deny": [ @@ -21,69 +21,69 @@ Describe 'security.sh' } } SETTINGS - } +} - cleanup() { - rm -rf "$TEMP_HOME" - } +cleanup() { + rm -rf "$TEMP_HOME" +} - Before 'setup' - After 'cleanup' +Before 'setup' +After 'cleanup' - Describe 'tool filtering' - It 'passes non-Bash tools through' - Data '{"tool": {"name": "Read", "input": {}}}' - When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" - The status should be success - End +Describe 'tool filtering' +It 'passes non-Bash tools through' +Data '{"tool": {"name": "Read", "input": {}}}' +When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" +The status should be success +End - It 'passes when no tool specified' - Data '{}' - When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" - The status should be success - End - End +It 'passes when no tool specified' +Data '{}' +When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" +The status should be success +End +End - Describe 'safe commands' - It 'allows ls -la' - Data '{"tool": {"name": "Bash", "input": {"command": "ls -la"}}}' - When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" - The status should be success - End +Describe 'safe commands' +It 'allows ls -la' +Data '{"tool": {"name": "Bash", "input": {"command": "ls -la"}}}' +When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" +The status should be success +End - It 'allows git status' - Data '{"tool": {"name": "Bash", "input": {"command": "git status"}}}' - When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" - The status should be success - End +It 'allows git status' +Data '{"tool": {"name": "Bash", "input": {"command": "git status"}}}' +When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" +The status should be success +End - It 'allows cat /etc/hosts' - Data '{"tool": {"name": "Bash", "input": {"command": "cat /etc/hosts"}}}' - When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" - The status should be success - End +It 'allows cat /etc/hosts' +Data '{"tool": {"name": "Bash", "input": {"command": "cat /etc/hosts"}}}' +When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" +The status should be success +End - It 'allows echo hello' - Data '{"tool": {"name": "Bash", "input": {"command": "echo hello"}}}' - When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" - The status should be success - End - End +It 'allows echo hello' +Data '{"tool": {"name": "Bash", "input": {"command": "echo hello"}}}' +When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" +The status should be success +End +End - Describe 'blocked commands' - It 'blocks rm -rf /*' - Data '{"tool": {"name": "Bash", "input": {"command": "rm -rf /*"}}}' - When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" - The status should eq 2 - The stderr should include 'BLOCKED' - End - End +Describe 'blocked commands' +It 'blocks rm -rf /*' +Data '{"tool": {"name": "Bash", "input": {"command": "rm -rf /*"}}}' +When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" +The status should eq 2 +The stderr should include 'BLOCKED' +End +End - Describe 'edge cases' - It 'passes with empty input' - Data '{"tool": {"name": "Bash", "input": {}}}' - When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" - The status should be success - End - End +Describe 'edge cases' +It 'passes with empty input' +Data '{"tool": {"name": "Bash", "input": {}}}' +When run bash -c "HOME='$TEMP_HOME' bash '$SCRIPT'" +The status should be success +End +End End From 6c3bf112d5d99218072569f211e7b8f54ca6e119 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 06:17:13 +0900 Subject: [PATCH 30/41] feat(scripts): fix shellspec wrapper script to support symlink execution --- overlays/default.nix | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/overlays/default.nix b/overlays/default.nix index 39265f679..680bae992 100644 --- a/overlays/default.nix +++ b/overlays/default.nix @@ -18,4 +18,19 @@ # Provide non-deprecated alias so upstream modules using pkgs.system don't emit warnings. system = prev.stdenv.hostPlatform.system; }) + (final: prev: { + # Fix shellspec wrapper script that breaks when called via symlinks + shellspec = prev.shellspec.overrideAttrs (oldAttrs: { + postInstall = + (oldAttrs.postInstall or "") + + '' + # Replace the wrapper with one that uses an absolute path + cat > $out/bin/shellspec << EOF + #!${prev.bash}/bin/sh + exec "$out/lib/shellspec/shellspec" "\$@" + EOF + chmod +x $out/bin/shellspec + ''; + }); + }) ] From 0ef2d9a2c1ab8f83e63c2347b2d2b1b21a11bd1a Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 06:18:49 +0900 Subject: [PATCH 31/41] feat(tests): refactor pushover_spec.sh to use mock curl setup --- spec/pushover_spec.sh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/spec/pushover_spec.sh b/spec/pushover_spec.sh index 25a845e06..7f29fff9e 100644 --- a/spec/pushover_spec.sh +++ b/spec/pushover_spec.sh @@ -18,13 +18,14 @@ Describe 'SessionEnd hook' setup() { export PUSHOVER_API_TOKEN="test_token" export PUSHOVER_USER_KEY="test_user" + # Create a mock curl that does nothing + MOCK_BIN=$(mktemp -d) + printf '#!/bin/sh\nexit 0\n' > "$MOCK_BIN/curl" + chmod +x "$MOCK_BIN/curl" + export PATH="$MOCK_BIN:$PATH" } Before 'setup' -mock_curl() { - echo "CURL_CALLED" -} - It 'skips notification for "other" reason' When run bash "$SCRIPT" <<<'{"reason": "other"}' The status should be success @@ -41,6 +42,11 @@ Describe 'Notification hook' setup() { export PUSHOVER_API_TOKEN="test_token" export PUSHOVER_USER_KEY="test_user" + # Create a mock curl that does nothing + MOCK_BIN=$(mktemp -d) + printf '#!/bin/sh\nexit 0\n' > "$MOCK_BIN/curl" + chmod +x "$MOCK_BIN/curl" + export PATH="$MOCK_BIN:$PATH" } Before 'setup' From 6df2499a1059187b437bc1c58d452fcd054830d0 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 06:19:02 +0900 Subject: [PATCH 32/41] fix(tests): adjust spacing in mock curl setup for consistency --- overlays/default.nix | 18 ++++++++---------- spec/pushover_spec.sh | 4 ++-- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/overlays/default.nix b/overlays/default.nix index 680bae992..0bab21d52 100644 --- a/overlays/default.nix +++ b/overlays/default.nix @@ -21,16 +21,14 @@ (final: prev: { # Fix shellspec wrapper script that breaks when called via symlinks shellspec = prev.shellspec.overrideAttrs (oldAttrs: { - postInstall = - (oldAttrs.postInstall or "") - + '' - # Replace the wrapper with one that uses an absolute path - cat > $out/bin/shellspec << EOF - #!${prev.bash}/bin/sh - exec "$out/lib/shellspec/shellspec" "\$@" - EOF - chmod +x $out/bin/shellspec - ''; + postInstall = (oldAttrs.postInstall or "") + '' + # Replace the wrapper with one that uses an absolute path + cat > $out/bin/shellspec << EOF + #!${prev.bash}/bin/sh + exec "$out/lib/shellspec/shellspec" "\$@" + EOF + chmod +x $out/bin/shellspec + ''; }); }) ] diff --git a/spec/pushover_spec.sh b/spec/pushover_spec.sh index 7f29fff9e..82d1dcfaa 100644 --- a/spec/pushover_spec.sh +++ b/spec/pushover_spec.sh @@ -20,7 +20,7 @@ setup() { export PUSHOVER_USER_KEY="test_user" # Create a mock curl that does nothing MOCK_BIN=$(mktemp -d) - printf '#!/bin/sh\nexit 0\n' > "$MOCK_BIN/curl" + printf '#!/bin/sh\nexit 0\n' >"$MOCK_BIN/curl" chmod +x "$MOCK_BIN/curl" export PATH="$MOCK_BIN:$PATH" } @@ -44,7 +44,7 @@ setup() { export PUSHOVER_USER_KEY="test_user" # Create a mock curl that does nothing MOCK_BIN=$(mktemp -d) - printf '#!/bin/sh\nexit 0\n' > "$MOCK_BIN/curl" + printf '#!/bin/sh\nexit 0\n' >"$MOCK_BIN/curl" chmod +x "$MOCK_BIN/curl" export PATH="$MOCK_BIN:$PATH" } From e33cb886e398b0fb4a949930744e4bcf46b49c40 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 06:21:32 +0900 Subject: [PATCH 33/41] chore: update --- spec/pushover_spec.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/pushover_spec.sh b/spec/pushover_spec.sh index 82d1dcfaa..956d1c410 100644 --- a/spec/pushover_spec.sh +++ b/spec/pushover_spec.sh @@ -24,7 +24,11 @@ setup() { chmod +x "$MOCK_BIN/curl" export PATH="$MOCK_BIN:$PATH" } +cleanup() { + rm -rf "$MOCK_BIN" +} Before 'setup' +After 'cleanup' It 'skips notification for "other" reason' When run bash "$SCRIPT" <<<'{"reason": "other"}' @@ -48,7 +52,11 @@ setup() { chmod +x "$MOCK_BIN/curl" export PATH="$MOCK_BIN:$PATH" } +cleanup() { + rm -rf "$MOCK_BIN" +} Before 'setup' +After 'cleanup' It 'skips login notification' When run bash "$SCRIPT" <<<'{"message": "Claude Code login successful"}' From 12b07a72e61068e8de03b6f203fbbbfefcd7a424 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 06:21:59 +0900 Subject: [PATCH 34/41] feat(tests): refactor notify_spec.sh to use mock osascript and cleanup functions --- spec/notify_spec.sh | 28 +++++++++++++++++++++------- spec/pushover_spec.sh | 4 +--- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/spec/notify_spec.sh b/spec/notify_spec.sh index fd8883870..2123e193e 100644 --- a/spec/notify_spec.sh +++ b/spec/notify_spec.sh @@ -20,31 +20,45 @@ End Describe 'Notification hook (no Pushover)' setup() { - unset PUSHOVER_API_TOKEN 2>/dev/null || true - unset PUSHOVER_USER_KEY 2>/dev/null || true + # Create mock osascript that does nothing + MOCK_BIN=$(mktemp -d) + printf '#!/bin/sh\nexit 0\n' >"$MOCK_BIN/osascript" + chmod +x "$MOCK_BIN/osascript" + export PATH="$MOCK_BIN:$PATH" +} +cleanup() { + rm -rf "$MOCK_BIN" } Before 'setup' +After 'cleanup' It 'exits 0 for login notification' -When run bash "$SCRIPT" <<<'{"message": "Claude Code login successful"}' +When run bash -c "unset PUSHOVER_API_TOKEN PUSHOVER_USER_KEY; bash '$SCRIPT'" <<<'{"message": "Claude Code login successful"}' The status should be success End It 'exits 0 for waiting notification' -When run bash "$SCRIPT" <<<'{"message": "Claude is waiting for your input"}' +When run bash -c "unset PUSHOVER_API_TOKEN PUSHOVER_USER_KEY; bash '$SCRIPT'" <<<'{"message": "Claude is waiting for your input"}' The status should be success End End Describe 'SessionEnd hook (no Pushover)' setup() { - unset PUSHOVER_API_TOKEN 2>/dev/null || true - unset PUSHOVER_USER_KEY 2>/dev/null || true + # Create mock osascript that does nothing + MOCK_BIN=$(mktemp -d) + printf '#!/bin/sh\nexit 0\n' >"$MOCK_BIN/osascript" + chmod +x "$MOCK_BIN/osascript" + export PATH="$MOCK_BIN:$PATH" +} +cleanup() { + rm -rf "$MOCK_BIN" } Before 'setup' +After 'cleanup' It 'exits 0 for session end' -When run bash "$SCRIPT" <<<'{"reason": "user_exit"}' +When run bash -c "unset PUSHOVER_API_TOKEN PUSHOVER_USER_KEY; bash '$SCRIPT'" <<<'{"reason": "user_exit"}' The status should be success End End diff --git a/spec/pushover_spec.sh b/spec/pushover_spec.sh index 956d1c410..5013bc9bc 100644 --- a/spec/pushover_spec.sh +++ b/spec/pushover_spec.sh @@ -6,9 +6,7 @@ SCRIPT="$PWD/config/claude/pushover.sh" Describe 'credential handling' It 'exits 0 when no credentials are set' -unset PUSHOVER_API_TOKEN 2>/dev/null || true -unset PUSHOVER_USER_KEY 2>/dev/null || true -When run bash "$SCRIPT" <<<'{}' +When run bash -c "unset PUSHOVER_API_TOKEN PUSHOVER_USER_KEY; bash '$SCRIPT'" <<<'{}' The status should be success The output should eq '' End From 4b82fb0cd10e79e092c456f1964db468717fc22c Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 06:22:39 +0900 Subject: [PATCH 35/41] fix(tests): update pushover_spec.sh to use fake HOME for credential handling test --- spec/pushover_spec.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/pushover_spec.sh b/spec/pushover_spec.sh index 5013bc9bc..a118a904f 100644 --- a/spec/pushover_spec.sh +++ b/spec/pushover_spec.sh @@ -6,7 +6,8 @@ SCRIPT="$PWD/config/claude/pushover.sh" Describe 'credential handling' It 'exits 0 when no credentials are set' -When run bash -c "unset PUSHOVER_API_TOKEN PUSHOVER_USER_KEY; bash '$SCRIPT'" <<<'{}' +# Use fake HOME so script cannot source real .env file +When run bash -c "HOME=/nonexistent bash '$SCRIPT'" <<<'{}' The status should be success The output should eq '' End From 0baa00257d7d1648c7304704f2acb64e821ea96d Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 06:22:44 +0900 Subject: [PATCH 36/41] fix(tests): update notify_spec.sh to use fake HOME for notification tests --- spec/notify_spec.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/spec/notify_spec.sh b/spec/notify_spec.sh index 2123e193e..a0c4fa573 100644 --- a/spec/notify_spec.sh +++ b/spec/notify_spec.sh @@ -33,12 +33,14 @@ Before 'setup' After 'cleanup' It 'exits 0 for login notification' -When run bash -c "unset PUSHOVER_API_TOKEN PUSHOVER_USER_KEY; bash '$SCRIPT'" <<<'{"message": "Claude Code login successful"}' +# Use fake HOME so script cannot source real .env file +When run bash -c "HOME=/nonexistent bash '$SCRIPT'" <<<'{"message": "Claude Code login successful"}' The status should be success End It 'exits 0 for waiting notification' -When run bash -c "unset PUSHOVER_API_TOKEN PUSHOVER_USER_KEY; bash '$SCRIPT'" <<<'{"message": "Claude is waiting for your input"}' +# Use fake HOME so script cannot source real .env file +When run bash -c "HOME=/nonexistent bash '$SCRIPT'" <<<'{"message": "Claude is waiting for your input"}' The status should be success End End @@ -58,7 +60,8 @@ Before 'setup' After 'cleanup' It 'exits 0 for session end' -When run bash -c "unset PUSHOVER_API_TOKEN PUSHOVER_USER_KEY; bash '$SCRIPT'" <<<'{"reason": "user_exit"}' +# Use fake HOME so script cannot source real .env file +When run bash -c "HOME=/nonexistent bash '$SCRIPT'" <<<'{"reason": "user_exit"}' The status should be success End End From 63655520ed8ee8cf491280ed1c7b2d4609a8a188 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 06:24:56 +0900 Subject: [PATCH 37/41] fix(tests): update pushover_spec.sh to unset credentials before running tests --- spec/pushover_spec.sh | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/spec/pushover_spec.sh b/spec/pushover_spec.sh index a118a904f..34a012a67 100644 --- a/spec/pushover_spec.sh +++ b/spec/pushover_spec.sh @@ -15,8 +15,6 @@ End Describe 'SessionEnd hook' setup() { - export PUSHOVER_API_TOKEN="test_token" - export PUSHOVER_USER_KEY="test_user" # Create a mock curl that does nothing MOCK_BIN=$(mktemp -d) printf '#!/bin/sh\nexit 0\n' >"$MOCK_BIN/curl" @@ -30,21 +28,20 @@ Before 'setup' After 'cleanup' It 'skips notification for "other" reason' -When run bash "$SCRIPT" <<<'{"reason": "other"}' +# Clear any inherited credentials, set test ones +When run bash -c "unset PUSHOVER_API_TOKEN PUSHOVER_USER_KEY; export PUSHOVER_API_TOKEN=test_token PUSHOVER_USER_KEY=test_user; bash '$SCRIPT'" <<<'{"reason": "other"}' The status should be success The output should eq '' End It 'processes notification for "user_exit" reason' -When run bash "$SCRIPT" <<<'{"reason": "user_exit"}' +When run bash -c "unset PUSHOVER_API_TOKEN PUSHOVER_USER_KEY; export PUSHOVER_API_TOKEN=test_token PUSHOVER_USER_KEY=test_user; bash '$SCRIPT'" <<<'{"reason": "user_exit"}' The status should be success End End Describe 'Notification hook' setup() { - export PUSHOVER_API_TOKEN="test_token" - export PUSHOVER_USER_KEY="test_user" # Create a mock curl that does nothing MOCK_BIN=$(mktemp -d) printf '#!/bin/sh\nexit 0\n' >"$MOCK_BIN/curl" @@ -58,12 +55,12 @@ Before 'setup' After 'cleanup' It 'skips login notification' -When run bash "$SCRIPT" <<<'{"message": "Claude Code login successful"}' +When run bash -c "unset PUSHOVER_API_TOKEN PUSHOVER_USER_KEY; export PUSHOVER_API_TOKEN=test_token PUSHOVER_USER_KEY=test_user; bash '$SCRIPT'" <<<'{"message": "Claude Code login successful"}' The status should be success End It 'processes waiting notification' -When run bash "$SCRIPT" <<<'{"message": "Claude is waiting for your input"}' +When run bash -c "unset PUSHOVER_API_TOKEN PUSHOVER_USER_KEY; export PUSHOVER_API_TOKEN=test_token PUSHOVER_USER_KEY=test_user; bash '$SCRIPT'" <<<'{"message": "Claude is waiting for your input"}' The status should be success End End From fc5dcaebb350b3ab0037eaf3c961c99db0f50779 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 06:29:24 +0900 Subject: [PATCH 38/41] fix(tests): enhance pushover_spec.sh to unset credentials for credential handling test --- spec/pushover_spec.sh | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/spec/pushover_spec.sh b/spec/pushover_spec.sh index 34a012a67..bd6287224 100644 --- a/spec/pushover_spec.sh +++ b/spec/pushover_spec.sh @@ -5,9 +5,22 @@ Describe 'pushover.sh' SCRIPT="$PWD/config/claude/pushover.sh" Describe 'credential handling' +setup() { + # Create a mock curl in case credentials leak through + MOCK_BIN=$(mktemp -d) + printf '#!/bin/sh\nexit 0\n' >"$MOCK_BIN/curl" + chmod +x "$MOCK_BIN/curl" + export PATH="$MOCK_BIN:$PATH" +} +cleanup() { + rm -rf "$MOCK_BIN" +} +Before 'setup' +After 'cleanup' + It 'exits 0 when no credentials are set' -# Use fake HOME so script cannot source real .env file -When run bash -c "HOME=/nonexistent bash '$SCRIPT'" <<<'{}' +# Use fake HOME and unset env vars so script cannot find credentials +When run bash -c "unset PUSHOVER_API_TOKEN PUSHOVER_USER_KEY; HOME=/nonexistent bash '$SCRIPT'" <<<'{}' The status should be success The output should eq '' End From e949d4e8d194b786155456b067deb2216a06dc17 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 10:21:28 +0900 Subject: [PATCH 39/41] fix(tests): update notify_spec.sh to use echo for message input in tests --- spec/notify_spec.sh | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/spec/notify_spec.sh b/spec/notify_spec.sh index a0c4fa573..7775a0c7a 100644 --- a/spec/notify_spec.sh +++ b/spec/notify_spec.sh @@ -12,7 +12,7 @@ setup() { Before 'setup' It 'exits early and skips local notification' -When run bash "$SCRIPT" <<<'{"message": "Test message"}' +When run bash -c 'echo "{\"message\": \"Test message\"}" | bash '"$SCRIPT" The status should be success The output should eq '' End @@ -25,6 +25,10 @@ setup() { printf '#!/bin/sh\nexit 0\n' >"$MOCK_BIN/osascript" chmod +x "$MOCK_BIN/osascript" export PATH="$MOCK_BIN:$PATH" + + # Unset Pushover credentials to ensure clean test environment + unset PUSHOVER_API_TOKEN + unset PUSHOVER_USER_KEY } cleanup() { rm -rf "$MOCK_BIN" @@ -34,13 +38,13 @@ After 'cleanup' It 'exits 0 for login notification' # Use fake HOME so script cannot source real .env file -When run bash -c "HOME=/nonexistent bash '$SCRIPT'" <<<'{"message": "Claude Code login successful"}' +When run bash -c 'echo "{\"message\": \"Claude Code login successful\"}" | env HOME=/nonexistent bash '"$SCRIPT" The status should be success End It 'exits 0 for waiting notification' # Use fake HOME so script cannot source real .env file -When run bash -c "HOME=/nonexistent bash '$SCRIPT'" <<<'{"message": "Claude is waiting for your input"}' +When run bash -c 'echo "{\"message\": \"Claude is waiting for your input\"}" | env HOME=/nonexistent bash '"$SCRIPT" The status should be success End End @@ -52,6 +56,10 @@ setup() { printf '#!/bin/sh\nexit 0\n' >"$MOCK_BIN/osascript" chmod +x "$MOCK_BIN/osascript" export PATH="$MOCK_BIN:$PATH" + + # Unset Pushover credentials to ensure clean test environment + unset PUSHOVER_API_TOKEN + unset PUSHOVER_USER_KEY } cleanup() { rm -rf "$MOCK_BIN" @@ -61,7 +69,7 @@ After 'cleanup' It 'exits 0 for session end' # Use fake HOME so script cannot source real .env file -When run bash -c "HOME=/nonexistent bash '$SCRIPT'" <<<'{"reason": "user_exit"}' +When run bash -c 'echo "{\"reason\": \"user_exit\"}" | env HOME=/nonexistent bash '"$SCRIPT" The status should be success End End From c5a1a1287f536e9d62c0724c1f3e5b5d28418879 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 10:22:14 +0900 Subject: [PATCH 40/41] fix(tests): simplify credential handling in pushover_spec.sh tests --- spec/pushover_spec.sh | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/spec/pushover_spec.sh b/spec/pushover_spec.sh index bd6287224..9786a3d95 100644 --- a/spec/pushover_spec.sh +++ b/spec/pushover_spec.sh @@ -11,6 +11,10 @@ setup() { printf '#!/bin/sh\nexit 0\n' >"$MOCK_BIN/curl" chmod +x "$MOCK_BIN/curl" export PATH="$MOCK_BIN:$PATH" + + # Unset credentials to ensure clean test environment + unset PUSHOVER_API_TOKEN + unset PUSHOVER_USER_KEY } cleanup() { rm -rf "$MOCK_BIN" @@ -19,8 +23,8 @@ Before 'setup' After 'cleanup' It 'exits 0 when no credentials are set' -# Use fake HOME and unset env vars so script cannot find credentials -When run bash -c "unset PUSHOVER_API_TOKEN PUSHOVER_USER_KEY; HOME=/nonexistent bash '$SCRIPT'" <<<'{}' +# Use fake HOME so script cannot find credentials +When run bash -c 'echo "{}" | env HOME=/nonexistent bash '"$SCRIPT" The status should be success The output should eq '' End @@ -33,6 +37,10 @@ setup() { printf '#!/bin/sh\nexit 0\n' >"$MOCK_BIN/curl" chmod +x "$MOCK_BIN/curl" export PATH="$MOCK_BIN:$PATH" + + # Set test credentials + export PUSHOVER_API_TOKEN="test_token" + export PUSHOVER_USER_KEY="test_user" } cleanup() { rm -rf "$MOCK_BIN" @@ -41,14 +49,13 @@ Before 'setup' After 'cleanup' It 'skips notification for "other" reason' -# Clear any inherited credentials, set test ones -When run bash -c "unset PUSHOVER_API_TOKEN PUSHOVER_USER_KEY; export PUSHOVER_API_TOKEN=test_token PUSHOVER_USER_KEY=test_user; bash '$SCRIPT'" <<<'{"reason": "other"}' +When run bash -c 'echo "{\"reason\": \"other\"}" | bash '"$SCRIPT" The status should be success The output should eq '' End It 'processes notification for "user_exit" reason' -When run bash -c "unset PUSHOVER_API_TOKEN PUSHOVER_USER_KEY; export PUSHOVER_API_TOKEN=test_token PUSHOVER_USER_KEY=test_user; bash '$SCRIPT'" <<<'{"reason": "user_exit"}' +When run bash -c 'echo "{\"reason\": \"user_exit\"}" | bash '"$SCRIPT" The status should be success End End @@ -60,6 +67,10 @@ setup() { printf '#!/bin/sh\nexit 0\n' >"$MOCK_BIN/curl" chmod +x "$MOCK_BIN/curl" export PATH="$MOCK_BIN:$PATH" + + # Set test credentials + export PUSHOVER_API_TOKEN="test_token" + export PUSHOVER_USER_KEY="test_user" } cleanup() { rm -rf "$MOCK_BIN" @@ -68,12 +79,12 @@ Before 'setup' After 'cleanup' It 'skips login notification' -When run bash -c "unset PUSHOVER_API_TOKEN PUSHOVER_USER_KEY; export PUSHOVER_API_TOKEN=test_token PUSHOVER_USER_KEY=test_user; bash '$SCRIPT'" <<<'{"message": "Claude Code login successful"}' +When run bash -c 'echo "{\"message\": \"Claude Code login successful\"}" | bash '"$SCRIPT" The status should be success End It 'processes waiting notification' -When run bash -c "unset PUSHOVER_API_TOKEN PUSHOVER_USER_KEY; export PUSHOVER_API_TOKEN=test_token PUSHOVER_USER_KEY=test_user; bash '$SCRIPT'" <<<'{"message": "Claude is waiting for your input"}' +When run bash -c 'echo "{\"message\": \"Claude is waiting for your input\"}" | bash '"$SCRIPT" The status should be success End End From 5ec29777a134062ac4fd0e90bf2499eeee324d38 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 10:23:37 +0900 Subject: [PATCH 41/41] fix(tests): change shellspec command to run in bash for compatibility --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2198b74a8..c12501309 100644 --- a/Makefile +++ b/Makefile @@ -630,7 +630,7 @@ git-submodule-sync: ## Sync and update git submodules. .PHONY: shell-test shell-test: ## Run shell script tests using ShellSpec. @echo "๐Ÿงช Running shell tests..." - @shellspec + @bash -c "shellspec" .PHONY: shell-test-dev shell-test-dev: ## Run shell tests inside the Nix dev shell (mirrors CI).