From 884e04074384a76ab2c22ab472fd3f28a574ffa3 Mon Sep 17 00:00:00 2001 From: Patrick Taylor <1963845+pstaylor-patrick@users.noreply.github.com> Date: Mon, 22 Jun 2026 09:32:15 -0500 Subject: [PATCH 1/2] fix(pst): suppress double browser-open for same URL within 60s Co-Authored-By: Claude Sonnet 4.6 --- skills/pst/scripts/hooks/pst-open-on-post.rb | 38 +++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/skills/pst/scripts/hooks/pst-open-on-post.rb b/skills/pst/scripts/hooks/pst-open-on-post.rb index 846e138..adfb6b9 100644 --- a/skills/pst/scripts/hooks/pst-open-on-post.rb +++ b/skills/pst/scripts/hooks/pst-open-on-post.rb @@ -5,6 +5,7 @@ # PR/issue/Jira description updated), open the resulting page in the browser so # he can see what was posted on his behalf (rule 17). Side effect only; this hook # NEVER blocks. Inert unless armed. Skip a single run with PST_NO_BROWSER=1. +require 'fileutils' require_relative 'pst_common' exit 0 unless Pst.armed? @@ -47,6 +48,41 @@ urls = urls.uniq.first(3) exit 0 if urls.empty? +# 60-second recency dedup: suppress double-opens caused by `gh pr create` +# followed immediately by `gh pr edit --body-file` for the same URL. +DEDUP_DIR = File.join(Dir.home, '.claude', 'pst') +DEDUP_FILE = File.join(DEDUP_DIR, 'open-dedup.json') +DEDUP_TTL = 60 + +def recently_opened?(url) + return false unless File.exist?(DEDUP_FILE) + + begin + entries = JSON.parse(File.read(DEDUP_FILE)) + cutoff = Time.now.to_i - DEDUP_TTL + entries.any? { |e| e['url'] == url && e['at'].to_i >= cutoff } + rescue StandardError + false + end +end + +def record_opened(url) + begin + FileUtils.mkdir_p(DEDUP_DIR) + entries = File.exist?(DEDUP_FILE) ? JSON.parse(File.read(DEDUP_FILE)) : [] + cutoff = Time.now.to_i - DEDUP_TTL + entries.reject! { |e| e['at'].to_i < cutoff } + entries << { 'url' => url, 'at' => Time.now.to_i } + File.write(DEDUP_FILE, JSON.generate(entries)) + rescue StandardError + # A write failure must never block the open. + end +end + opener = RUBY_PLATFORM =~ /darwin/ ? 'open' : 'xdg-open' -urls.each { |u| system(opener, u, %i[out err] => File::NULL) } +urls.each do |u| + next if recently_opened?(u) + record_opened(u) + system(opener, u, %i[out err] => File::NULL) +end exit 0 From 04a27f11c4d46365332756c15af1fc649258e1f3 Mon Sep 17 00:00:00 2001 From: Patrick Taylor <1963845+pstaylor-patrick@users.noreply.github.com> Date: Mon, 22 Jun 2026 09:37:59 -0500 Subject: [PATCH 2/2] fix(pst): suppress double browser-open for same URL (10s dedup) gh pr create then gh pr edit --body-file fire the hook back-to-back for the same URL, opening the browser twice. Fix: 10-second recency dedup cache at ~/.cache/pst-hooks/open-on-post-dedup.json. Atomic write (tmp+rename), self-healing on corrupt JSON, cross-session scope documented in comments. Co-Authored-By: Claude Sonnet 4.6 --- skills/pst/scripts/hooks/pst-open-on-post.rb | 80 ++++++++++++-------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/skills/pst/scripts/hooks/pst-open-on-post.rb b/skills/pst/scripts/hooks/pst-open-on-post.rb index adfb6b9..828250b 100644 --- a/skills/pst/scripts/hooks/pst-open-on-post.rb +++ b/skills/pst/scripts/hooks/pst-open-on-post.rb @@ -6,8 +6,56 @@ # he can see what was posted on his behalf (rule 17). Side effect only; this hook # NEVER blocks. Inert unless armed. Skip a single run with PST_NO_BROWSER=1. require 'fileutils' +require 'json' require_relative 'pst_common' +# --- Dedup: prevent the same URL from being opened twice in quick succession. +# `gh pr create` fires a PostToolUse event and then `gh pr edit` fires another +# within milliseconds for the same PR URL. Without dedup the browser tab opens +# twice. The dedup file is global (not session-scoped), which is intentional: +# if two sessions open the same URL within the TTL window, the second is +# suppressed -- the consequence is only a skipped browser tab, which is fine. +DEDUP_DIR = File.expand_path('~/.cache/pst-hooks') +DEDUP_FILE = File.join(DEDUP_DIR, 'open-on-post-dedup.json') +DEDUP_TTL = 10 # seconds -- double-fires happen within milliseconds; 10s is generous + +def recently_opened?(url) + return false unless File.exist?(DEDUP_FILE) + + begin + entries = JSON.parse(File.read(DEDUP_FILE)) + cutoff = Time.now.to_i - DEDUP_TTL + entries.any? { |e| e['url'] == url && e['at'].to_i >= cutoff } + rescue JSON::ParserError + File.delete(DEDUP_FILE) rescue nil + false + rescue StandardError + false + end +end + +def record_opened(url) + FileUtils.mkdir_p(DEDUP_DIR) + entries = if File.exist?(DEDUP_FILE) + begin + JSON.parse(File.read(DEDUP_FILE)) + rescue JSON::ParserError + File.delete(DEDUP_FILE) rescue nil + [] + end + else + [] + end + cutoff = Time.now.to_i - DEDUP_TTL + entries.reject! { |e| e['at'].to_i < cutoff } + entries << { 'url' => url, 'at' => Time.now.to_i } + tmp = DEDUP_FILE + '.tmp' + File.write(tmp, JSON.generate(entries)) + File.rename(tmp, DEDUP_FILE) +rescue StandardError + # A write failure must never block the open. +end + exit 0 unless Pst.armed? exit 0 if ENV['PST_NO_BROWSER'] == '1' @@ -48,40 +96,10 @@ urls = urls.uniq.first(3) exit 0 if urls.empty? -# 60-second recency dedup: suppress double-opens caused by `gh pr create` -# followed immediately by `gh pr edit --body-file` for the same URL. -DEDUP_DIR = File.join(Dir.home, '.claude', 'pst') -DEDUP_FILE = File.join(DEDUP_DIR, 'open-dedup.json') -DEDUP_TTL = 60 - -def recently_opened?(url) - return false unless File.exist?(DEDUP_FILE) - - begin - entries = JSON.parse(File.read(DEDUP_FILE)) - cutoff = Time.now.to_i - DEDUP_TTL - entries.any? { |e| e['url'] == url && e['at'].to_i >= cutoff } - rescue StandardError - false - end -end - -def record_opened(url) - begin - FileUtils.mkdir_p(DEDUP_DIR) - entries = File.exist?(DEDUP_FILE) ? JSON.parse(File.read(DEDUP_FILE)) : [] - cutoff = Time.now.to_i - DEDUP_TTL - entries.reject! { |e| e['at'].to_i < cutoff } - entries << { 'url' => url, 'at' => Time.now.to_i } - File.write(DEDUP_FILE, JSON.generate(entries)) - rescue StandardError - # A write failure must never block the open. - end -end - opener = RUBY_PLATFORM =~ /darwin/ ? 'open' : 'xdg-open' urls.each do |u| next if recently_opened?(u) + record_opened(u) system(opener, u, %i[out err] => File::NULL) end