Security: cfdude/pm

SECURITY.md

Security Policy

Supported versions

pm ships as a single, always-current Claude Code plugin — only the latest released version is supported. There is no LTS branch; upgrading is a /pm:upgrade away and is designed to be safe to run at any time (see CHANGELOG.md for release-by-release upgrade notes).

Reporting a vulnerability

Please do not open a public GitHub issue for a security vulnerability. Instead, use GitHub's private vulnerability reporting for this repository, or email the maintainer directly (see the author field in .claude-plugin/plugin.json).

Include what you'd include in any good bug report: the affected version (pmVersion in .conductor/state.json, or the plugin version), a description of the issue, and reproduction steps if you have them.

Scope and architecture

pm's engine (scripts/conductor.mjs) is a zero-dependency Node.js CLI — no npm packages, no package.json dependencies, no supply chain beyond Node's own built-ins. It is also an instruction layer, not an integration layer: the engine itself never opens a network connection or calls an external system (Jira, GitHub, Linear, etc.) — it only shapes instructions the interactive Claude Code agent acts on with its own tooling. This significantly narrows the engine's own attack surface; most of what a security report would concern is either in that instruction-shaping logic or in how a consuming repo's agent acts on it.

The one place the engine executes code it did not ship

pm installs four hooks — SessionStart, PreToolUse, PostToolUse, PreCompact — which run in every project on the machine, initialized or not, whether or not that project has ever run /pm:init. With one exception, everything they execute ships with the plugin.

The exception is the self-hosting handoff (scripts/lib/self-hosting.mjs): so that developing pm does not run an engine a release behind the checkout, the installed engine can re-exec <project>/scripts/conductor.mjs. Because that is project-supplied code, the decision to do it is opt-in and comes solely from the environment: it happens only when PM_ENGINE_DELEGATION is set to an absolute path that resolves to the same tree as the project being run in. Unset — the default, and what every ordinary user sees — no handoff is ever considered.

Nothing readable from inside a repository can enable this. In particular the presence of scripts/conductor.mjs, or a .claude-plugin/plugin.json naming pm, grants nothing: those are sanity checks applied after authorization, on a path the user has already named, and they are not a credential — a repository can write both. Any change that lets a project's own contents influence whether its code is executed is a vulnerability in this file's terms, and is worth reporting even if it looks like a convenience.

The instruction surfaces the agent acts on

The engine is not the only thing that can name a path for node to run. commands/*.md, skills/, agents/ and hooks/ are instructions an interactive agent executes, so an engine path written there is executed just as surely as one the engine computes. Through 0.29.0 fourteen of those files resolved the engine from $CLAUDE_PROJECT_DIR on nothing but an -f test, which handed any project carrying a scripts/conductor.mjs a node invocation whenever a user typed a /pm:* command (#139). That arm is gone: the shipped surfaces now resolve the engine only from the plugin — $CLAUDE_PLUGIN_ROOT, else the newest copy in ~/.claude/plugins/cache/ — and scripts/test/engine-resolution.test.mjs fails CI if any file under those directories reintroduces either resolution shape it matches: the ${CLAUDE_PROJECT_DIR:+…} expansion that shipped, or a direct $CLAUDE_PROJECT_DIR/scripts/conductor.mjs. It is a regression guard against the pattern that appeared, not a proof that no indirection could express the same idea — a report of one that slips past it is welcome.

Automated scanning

This repository runs Semgrep (SAST) and Trivy (filesystem vulnerability scanning) on every push and pull request to main, plus a weekly scheduled run, with results published to the repository's Security tab. main is branch-protected: no direct pushes, required CI status check, required commit signatures, enforced for admins too.

There aren't any published security advisories

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Security: cfdude/pm

SECURITY.md

Security Policy

Supported versions

pm ships as a single, always-current Claude Code plugin — only the latest released version is supported. There is no LTS branch; upgrading is a /pm:upgrade away and is designed to be safe to run at any time (see CHANGELOG.md for release-by-release upgrade notes).

Reporting a vulnerability

Please do not open a public GitHub issue for a security vulnerability. Instead, use GitHub's private vulnerability reporting for this repository, or email the maintainer directly (see the author field in .claude-plugin/plugin.json).

Include what you'd include in any good bug report: the affected version (pmVersion in .conductor/state.json, or the plugin version), a description of the issue, and reproduction steps if you have them.

Scope and architecture

pm's engine (scripts/conductor.mjs) is a zero-dependency Node.js CLI — no npm packages, no package.json dependencies, no supply chain beyond Node's own built-ins. It is also an instruction layer, not an integration layer: the engine itself never opens a network connection or calls an external system (Jira, GitHub, Linear, etc.) — it only shapes instructions the interactive Claude Code agent acts on with its own tooling. This significantly narrows the engine's own attack surface; most of what a security report would concern is either in that instruction-shaping logic or in how a consuming repo's agent acts on it.

The one place the engine executes code it did not ship

pm installs four hooks — SessionStart, PreToolUse, PostToolUse, PreCompact — which run in every project on the machine, initialized or not, whether or not that project has ever run /pm:init. With one exception, everything they execute ships with the plugin.

The exception is the self-hosting handoff (scripts/lib/self-hosting.mjs): so that developing pm does not run an engine a release behind the checkout, the installed engine can re-exec <project>/scripts/conductor.mjs. Because that is project-supplied code, the decision to do it is opt-in and comes solely from the environment: it happens only when PM_ENGINE_DELEGATION is set to an absolute path that resolves to the same tree as the project being run in. Unset — the default, and what every ordinary user sees — no handoff is ever considered.

Nothing readable from inside a repository can enable this. In particular the presence of scripts/conductor.mjs, or a .claude-plugin/plugin.json naming pm, grants nothing: those are sanity checks applied after authorization, on a path the user has already named, and they are not a credential — a repository can write both. Any change that lets a project's own contents influence whether its code is executed is a vulnerability in this file's terms, and is worth reporting even if it looks like a convenience.

The instruction surfaces the agent acts on

The engine is not the only thing that can name a path for node to run. commands/*.md, skills/, agents/ and hooks/ are instructions an interactive agent executes, so an engine path written there is executed just as surely as one the engine computes. Through 0.29.0 fourteen of those files resolved the engine from $CLAUDE_PROJECT_DIR on nothing but an -f test, which handed any project carrying a scripts/conductor.mjs a node invocation whenever a user typed a /pm:* command (#139). That arm is gone: the shipped surfaces now resolve the engine only from the plugin — $CLAUDE_PLUGIN_ROOT, else the newest copy in ~/.claude/plugins/cache/ — and scripts/test/engine-resolution.test.mjs fails CI if any file under those directories reintroduces either resolution shape it matches: the ${CLAUDE_PROJECT_DIR:+…} expansion that shipped, or a direct $CLAUDE_PROJECT_DIR/scripts/conductor.mjs. It is a regression guard against the pattern that appeared, not a proof that no indirection could express the same idea — a report of one that slips past it is welcome.

Automated scanning

This repository runs Semgrep (SAST) and Trivy (filesystem vulnerability scanning) on every push and pull request to main, plus a weekly scheduled run, with results published to the repository's Security tab. main is branch-protected: no direct pushes, required CI status check, required commit signatures, enforced for admins too.

There aren't any published security advisories

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Security: cfdude/pm

SECURITY.md

Security Policy

Supported versions

pm ships as a single, always-current Claude Code plugin — only the latest released version is supported. There is no LTS branch; upgrading is a /pm:upgrade away and is designed to be safe to run at any time (see CHANGELOG.md for release-by-release upgrade notes).

Reporting a vulnerability

Please do not open a public GitHub issue for a security vulnerability. Instead, use GitHub's private vulnerability reporting for this repository, or email the maintainer directly (see the author field in .claude-plugin/plugin.json).

Include what you'd include in any good bug report: the affected version (pmVersion in .conductor/state.json, or the plugin version), a description of the issue, and reproduction steps if you have them.

Scope and architecture

pm's engine (scripts/conductor.mjs) is a zero-dependency Node.js CLI — no npm packages, no package.json dependencies, no supply chain beyond Node's own built-ins. It is also an instruction layer, not an integration layer: the engine itself never opens a network connection or calls an external system (Jira, GitHub, Linear, etc.) — it only shapes instructions the interactive Claude Code agent acts on with its own tooling. This significantly narrows the engine's own attack surface; most of what a security report would concern is either in that instruction-shaping logic or in how a consuming repo's agent acts on it.

The one place the engine executes code it did not ship

pm installs four hooks — SessionStart, PreToolUse, PostToolUse, PreCompact — which run in every project on the machine, initialized or not, whether or not that project has ever run /pm:init. With one exception, everything they execute ships with the plugin.

The exception is the self-hosting handoff (scripts/lib/self-hosting.mjs): so that developing pm does not run an engine a release behind the checkout, the installed engine can re-exec <project>/scripts/conductor.mjs. Because that is project-supplied code, the decision to do it is opt-in and comes solely from the environment: it happens only when PM_ENGINE_DELEGATION is set to an absolute path that resolves to the same tree as the project being run in. Unset — the default, and what every ordinary user sees — no handoff is ever considered.

Nothing readable from inside a repository can enable this. In particular the presence of scripts/conductor.mjs, or a .claude-plugin/plugin.json naming pm, grants nothing: those are sanity checks applied after authorization, on a path the user has already named, and they are not a credential — a repository can write both. Any change that lets a project's own contents influence whether its code is executed is a vulnerability in this file's terms, and is worth reporting even if it looks like a convenience.

The instruction surfaces the agent acts on

The engine is not the only thing that can name a path for node to run. commands/*.md, skills/, agents/ and hooks/ are instructions an interactive agent executes, so an engine path written there is executed just as surely as one the engine computes. Through 0.29.0 fourteen of those files resolved the engine from $CLAUDE_PROJECT_DIR on nothing but an -f test, which handed any project carrying a scripts/conductor.mjs a node invocation whenever a user typed a /pm:* command (#139). That arm is gone: the shipped surfaces now resolve the engine only from the plugin — $CLAUDE_PLUGIN_ROOT, else the newest copy in ~/.claude/plugins/cache/ — and scripts/test/engine-resolution.test.mjs fails CI if any file under those directories reintroduces either resolution shape it matches: the ${CLAUDE_PROJECT_DIR:+…} expansion that shipped, or a direct $CLAUDE_PROJECT_DIR/scripts/conductor.mjs. It is a regression guard against the pattern that appeared, not a proof that no indirection could express the same idea — a report of one that slips past it is welcome.

Automated scanning

This repository runs Semgrep (SAST) and Trivy (filesystem vulnerability scanning) on every push and pull request to main, plus a weekly scheduled run, with results published to the repository's Security tab. main is branch-protected: no direct pushes, required CI status check, required commit signatures, enforced for admins too.

There aren't any published security advisories

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Security: cfdude/pm

SECURITY.md

Security Policy

Supported versions

pm ships as a single, always-current Claude Code plugin — only the latest released version is supported. There is no LTS branch; upgrading is a /pm:upgrade away and is designed to be safe to run at any time (see CHANGELOG.md for release-by-release upgrade notes).

Reporting a vulnerability

Please do not open a public GitHub issue for a security vulnerability. Instead, use GitHub's private vulnerability reporting for this repository, or email the maintainer directly (see the author field in .claude-plugin/plugin.json).

Include what you'd include in any good bug report: the affected version (pmVersion in .conductor/state.json, or the plugin version), a description of the issue, and reproduction steps if you have them.

Scope and architecture

pm's engine (scripts/conductor.mjs) is a zero-dependency Node.js CLI — no npm packages, no package.json dependencies, no supply chain beyond Node's own built-ins. It is also an instruction layer, not an integration layer: the engine itself never opens a network connection or calls an external system (Jira, GitHub, Linear, etc.) — it only shapes instructions the interactive Claude Code agent acts on with its own tooling. This significantly narrows the engine's own attack surface; most of what a security report would concern is either in that instruction-shaping logic or in how a consuming repo's agent acts on it.

The one place the engine executes code it did not ship

pm installs four hooks — SessionStart, PreToolUse, PostToolUse, PreCompact — which run in every project on the machine, initialized or not, whether or not that project has ever run /pm:init. With one exception, everything they execute ships with the plugin.

The exception is the self-hosting handoff (scripts/lib/self-hosting.mjs): so that developing pm does not run an engine a release behind the checkout, the installed engine can re-exec <project>/scripts/conductor.mjs. Because that is project-supplied code, the decision to do it is opt-in and comes solely from the environment: it happens only when PM_ENGINE_DELEGATION is set to an absolute path that resolves to the same tree as the project being run in. Unset — the default, and what every ordinary user sees — no handoff is ever considered.

Nothing readable from inside a repository can enable this. In particular the presence of scripts/conductor.mjs, or a .claude-plugin/plugin.json naming pm, grants nothing: those are sanity checks applied after authorization, on a path the user has already named, and they are not a credential — a repository can write both. Any change that lets a project's own contents influence whether its code is executed is a vulnerability in this file's terms, and is worth reporting even if it looks like a convenience.

The instruction surfaces the agent acts on

The engine is not the only thing that can name a path for node to run. commands/*.md, skills/, agents/ and hooks/ are instructions an interactive agent executes, so an engine path written there is executed just as surely as one the engine computes. Through 0.29.0 fourteen of those files resolved the engine from $CLAUDE_PROJECT_DIR on nothing but an -f test, which handed any project carrying a scripts/conductor.mjs a node invocation whenever a user typed a /pm:* command (#139). That arm is gone: the shipped surfaces now resolve the engine only from the plugin — $CLAUDE_PLUGIN_ROOT, else the newest copy in ~/.claude/plugins/cache/ — and scripts/test/engine-resolution.test.mjs fails CI if any file under those directories reintroduces either resolution shape it matches: the ${CLAUDE_PROJECT_DIR:+…} expansion that shipped, or a direct $CLAUDE_PROJECT_DIR/scripts/conductor.mjs. It is a regression guard against the pattern that appeared, not a proof that no indirection could express the same idea — a report of one that slips past it is welcome.

Automated scanning

This repository runs Semgrep (SAST) and Trivy (filesystem vulnerability scanning) on every push and pull request to main, plus a weekly scheduled run, with results published to the repository's Security tab. main is branch-protected: no direct pushes, required CI status check, required commit signatures, enforced for admins too.

There aren't any published security advisories

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Security: cfdude/pm

SECURITY.md

Security Policy

Supported versions

pm ships as a single, always-current Claude Code plugin — only the latest released version is supported. There is no LTS branch; upgrading is a /pm:upgrade away and is designed to be safe to run at any time (see CHANGELOG.md for release-by-release upgrade notes).

Reporting a vulnerability

Please do not open a public GitHub issue for a security vulnerability. Instead, use GitHub's private vulnerability reporting for this repository, or email the maintainer directly (see the author field in .claude-plugin/plugin.json).

Include what you'd include in any good bug report: the affected version (pmVersion in .conductor/state.json, or the plugin version), a description of the issue, and reproduction steps if you have them.

Scope and architecture

pm's engine (scripts/conductor.mjs) is a zero-dependency Node.js CLI — no npm packages, no package.json dependencies, no supply chain beyond Node's own built-ins. It is also an instruction layer, not an integration layer: the engine itself never opens a network connection or calls an external system (Jira, GitHub, Linear, etc.) — it only shapes instructions the interactive Claude Code agent acts on with its own tooling. This significantly narrows the engine's own attack surface; most of what a security report would concern is either in that instruction-shaping logic or in how a consuming repo's agent acts on it.

The one place the engine executes code it did not ship

pm installs four hooks — SessionStart, PreToolUse, PostToolUse, PreCompact — which run in every project on the machine, initialized or not, whether or not that project has ever run /pm:init. With one exception, everything they execute ships with the plugin.

The exception is the self-hosting handoff (scripts/lib/self-hosting.mjs): so that developing pm does not run an engine a release behind the checkout, the installed engine can re-exec <project>/scripts/conductor.mjs. Because that is project-supplied code, the decision to do it is opt-in and comes solely from the environment: it happens only when PM_ENGINE_DELEGATION is set to an absolute path that resolves to the same tree as the project being run in. Unset — the default, and what every ordinary user sees — no handoff is ever considered.

Nothing readable from inside a repository can enable this. In particular the presence of scripts/conductor.mjs, or a .claude-plugin/plugin.json naming pm, grants nothing: those are sanity checks applied after authorization, on a path the user has already named, and they are not a credential — a repository can write both. Any change that lets a project's own contents influence whether its code is executed is a vulnerability in this file's terms, and is worth reporting even if it looks like a convenience.

The instruction surfaces the agent acts on

The engine is not the only thing that can name a path for node to run. commands/*.md, skills/, agents/ and hooks/ are instructions an interactive agent executes, so an engine path written there is executed just as surely as one the engine computes. Through 0.29.0 fourteen of those files resolved the engine from $CLAUDE_PROJECT_DIR on nothing but an -f test, which handed any project carrying a scripts/conductor.mjs a node invocation whenever a user typed a /pm:* command (#139). That arm is gone: the shipped surfaces now resolve the engine only from the plugin — $CLAUDE_PLUGIN_ROOT, else the newest copy in ~/.claude/plugins/cache/ — and scripts/test/engine-resolution.test.mjs fails CI if any file under those directories reintroduces either resolution shape it matches: the ${CLAUDE_PROJECT_DIR:+…} expansion that shipped, or a direct $CLAUDE_PROJECT_DIR/scripts/conductor.mjs. It is a regression guard against the pattern that appeared, not a proof that no indirection could express the same idea — a report of one that slips past it is welcome.

Automated scanning

This repository runs Semgrep (SAST) and Trivy (filesystem vulnerability scanning) on every push and pull request to main, plus a weekly scheduled run, with results published to the repository's Security tab. main is branch-protected: no direct pushes, required CI status check, required commit signatures, enforced for admins too.

There aren't any published security advisories

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Security: cfdude/pm

SECURITY.md

Security Policy

Supported versions

pm ships as a single, always-current Claude Code plugin — only the latest released version is supported. There is no LTS branch; upgrading is a /pm:upgrade away and is designed to be safe to run at any time (see CHANGELOG.md for release-by-release upgrade notes).

Reporting a vulnerability

Please do not open a public GitHub issue for a security vulnerability. Instead, use GitHub's private vulnerability reporting for this repository, or email the maintainer directly (see the author field in .claude-plugin/plugin.json).

Include what you'd include in any good bug report: the affected version (pmVersion in .conductor/state.json, or the plugin version), a description of the issue, and reproduction steps if you have them.

Scope and architecture

pm's engine (scripts/conductor.mjs) is a zero-dependency Node.js CLI — no npm packages, no package.json dependencies, no supply chain beyond Node's own built-ins. It is also an instruction layer, not an integration layer: the engine itself never opens a network connection or calls an external system (Jira, GitHub, Linear, etc.) — it only shapes instructions the interactive Claude Code agent acts on with its own tooling. This significantly narrows the engine's own attack surface; most of what a security report would concern is either in that instruction-shaping logic or in how a consuming repo's agent acts on it.

The one place the engine executes code it did not ship

pm installs four hooks — SessionStart, PreToolUse, PostToolUse, PreCompact — which run in every project on the machine, initialized or not, whether or not that project has ever run /pm:init. With one exception, everything they execute ships with the plugin.

The exception is the self-hosting handoff (scripts/lib/self-hosting.mjs): so that developing pm does not run an engine a release behind the checkout, the installed engine can re-exec <project>/scripts/conductor.mjs. Because that is project-supplied code, the decision to do it is opt-in and comes solely from the environment: it happens only when PM_ENGINE_DELEGATION is set to an absolute path that resolves to the same tree as the project being run in. Unset — the default, and what every ordinary user sees — no handoff is ever considered.

Nothing readable from inside a repository can enable this. In particular the presence of scripts/conductor.mjs, or a .claude-plugin/plugin.json naming pm, grants nothing: those are sanity checks applied after authorization, on a path the user has already named, and they are not a credential — a repository can write both. Any change that lets a project's own contents influence whether its code is executed is a vulnerability in this file's terms, and is worth reporting even if it looks like a convenience.

The instruction surfaces the agent acts on

The engine is not the only thing that can name a path for node to run. commands/*.md, skills/, agents/ and hooks/ are instructions an interactive agent executes, so an engine path written there is executed just as surely as one the engine computes. Through 0.29.0 fourteen of those files resolved the engine from $CLAUDE_PROJECT_DIR on nothing but an -f test, which handed any project carrying a scripts/conductor.mjs a node invocation whenever a user typed a /pm:* command (#139). That arm is gone: the shipped surfaces now resolve the engine only from the plugin — $CLAUDE_PLUGIN_ROOT, else the newest copy in ~/.claude/plugins/cache/ — and scripts/test/engine-resolution.test.mjs fails CI if any file under those directories reintroduces either resolution shape it matches: the ${CLAUDE_PROJECT_DIR:+…} expansion that shipped, or a direct $CLAUDE_PROJECT_DIR/scripts/conductor.mjs. It is a regression guard against the pattern that appeared, not a proof that no indirection could express the same idea — a report of one that slips past it is welcome.

Automated scanning

This repository runs Semgrep (SAST) and Trivy (filesystem vulnerability scanning) on every push and pull request to main, plus a weekly scheduled run, with results published to the repository's Security tab. main is branch-protected: no direct pushes, required CI status check, required commit signatures, enforced for admins too.

There aren't any published security advisories

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Security: cfdude/pm

SECURITY.md

Security Policy

Supported versions

pm ships as a single, always-current Claude Code plugin — only the latest released version is supported. There is no LTS branch; upgrading is a /pm:upgrade away and is designed to be safe to run at any time (see CHANGELOG.md for release-by-release upgrade notes).

Reporting a vulnerability

Please do not open a public GitHub issue for a security vulnerability. Instead, use GitHub's private vulnerability reporting for this repository, or email the maintainer directly (see the author field in .claude-plugin/plugin.json).

Include what you'd include in any good bug report: the affected version (pmVersion in .conductor/state.json, or the plugin version), a description of the issue, and reproduction steps if you have them.

Scope and architecture

pm's engine (scripts/conductor.mjs) is a zero-dependency Node.js CLI — no npm packages, no package.json dependencies, no supply chain beyond Node's own built-ins. It is also an instruction layer, not an integration layer: the engine itself never opens a network connection or calls an external system (Jira, GitHub, Linear, etc.) — it only shapes instructions the interactive Claude Code agent acts on with its own tooling. This significantly narrows the engine's own attack surface; most of what a security report would concern is either in that instruction-shaping logic or in how a consuming repo's agent acts on it.

The one place the engine executes code it did not ship

pm installs four hooks — SessionStart, PreToolUse, PostToolUse, PreCompact — which run in every project on the machine, initialized or not, whether or not that project has ever run /pm:init. With one exception, everything they execute ships with the plugin.

The exception is the self-hosting handoff (scripts/lib/self-hosting.mjs): so that developing pm does not run an engine a release behind the checkout, the installed engine can re-exec <project>/scripts/conductor.mjs. Because that is project-supplied code, the decision to do it is opt-in and comes solely from the environment: it happens only when PM_ENGINE_DELEGATION is set to an absolute path that resolves to the same tree as the project being run in. Unset — the default, and what every ordinary user sees — no handoff is ever considered.

Nothing readable from inside a repository can enable this. In particular the presence of scripts/conductor.mjs, or a .claude-plugin/plugin.json naming pm, grants nothing: those are sanity checks applied after authorization, on a path the user has already named, and they are not a credential — a repository can write both. Any change that lets a project's own contents influence whether its code is executed is a vulnerability in this file's terms, and is worth reporting even if it looks like a convenience.

The instruction surfaces the agent acts on

The engine is not the only thing that can name a path for node to run. commands/*.md, skills/, agents/ and hooks/ are instructions an interactive agent executes, so an engine path written there is executed just as surely as one the engine computes. Through 0.29.0 fourteen of those files resolved the engine from $CLAUDE_PROJECT_DIR on nothing but an -f test, which handed any project carrying a scripts/conductor.mjs a node invocation whenever a user typed a /pm:* command (#139). That arm is gone: the shipped surfaces now resolve the engine only from the plugin — $CLAUDE_PLUGIN_ROOT, else the newest copy in ~/.claude/plugins/cache/ — and scripts/test/engine-resolution.test.mjs fails CI if any file under those directories reintroduces either resolution shape it matches: the ${CLAUDE_PROJECT_DIR:+…} expansion that shipped, or a direct $CLAUDE_PROJECT_DIR/scripts/conductor.mjs. It is a regression guard against the pattern that appeared, not a proof that no indirection could express the same idea — a report of one that slips past it is welcome.

Automated scanning

This repository runs Semgrep (SAST) and Trivy (filesystem vulnerability scanning) on every push and pull request to main, plus a weekly scheduled run, with results published to the repository's Security tab. main is branch-protected: no direct pushes, required CI status check, required commit signatures, enforced for admins too.

There aren't any published security advisories

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Security: cfdude/pm

SECURITY.md

Security Policy

Supported versions

pm ships as a single, always-current Claude Code plugin — only the latest released version is supported. There is no LTS branch; upgrading is a /pm:upgrade away and is designed to be safe to run at any time (see CHANGELOG.md for release-by-release upgrade notes).

Reporting a vulnerability

Please do not open a public GitHub issue for a security vulnerability. Instead, use GitHub's private vulnerability reporting for this repository, or email the maintainer directly (see the author field in .claude-plugin/plugin.json).

Include what you'd include in any good bug report: the affected version (pmVersion in .conductor/state.json, or the plugin version), a description of the issue, and reproduction steps if you have them.

Scope and architecture

pm's engine (scripts/conductor.mjs) is a zero-dependency Node.js CLI — no npm packages, no package.json dependencies, no supply chain beyond Node's own built-ins. It is also an instruction layer, not an integration layer: the engine itself never opens a network connection or calls an external system (Jira, GitHub, Linear, etc.) — it only shapes instructions the interactive Claude Code agent acts on with its own tooling. This significantly narrows the engine's own attack surface; most of what a security report would concern is either in that instruction-shaping logic or in how a consuming repo's agent acts on it.

The one place the engine executes code it did not ship

pm installs four hooks — SessionStart, PreToolUse, PostToolUse, PreCompact — which run in every project on the machine, initialized or not, whether or not that project has ever run /pm:init. With one exception, everything they execute ships with the plugin.

The exception is the self-hosting handoff (scripts/lib/self-hosting.mjs): so that developing pm does not run an engine a release behind the checkout, the installed engine can re-exec <project>/scripts/conductor.mjs. Because that is project-supplied code, the decision to do it is opt-in and comes solely from the environment: it happens only when PM_ENGINE_DELEGATION is set to an absolute path that resolves to the same tree as the project being run in. Unset — the default, and what every ordinary user sees — no handoff is ever considered.

Nothing readable from inside a repository can enable this. In particular the presence of scripts/conductor.mjs, or a .claude-plugin/plugin.json naming pm, grants nothing: those are sanity checks applied after authorization, on a path the user has already named, and they are not a credential — a repository can write both. Any change that lets a project's own contents influence whether its code is executed is a vulnerability in this file's terms, and is worth reporting even if it looks like a convenience.

The instruction surfaces the agent acts on

The engine is not the only thing that can name a path for node to run. commands/*.md, skills/, agents/ and hooks/ are instructions an interactive agent executes, so an engine path written there is executed just as surely as one the engine computes. Through 0.29.0 fourteen of those files resolved the engine from $CLAUDE_PROJECT_DIR on nothing but an -f test, which handed any project carrying a scripts/conductor.mjs a node invocation whenever a user typed a /pm:* command (#139). That arm is gone: the shipped surfaces now resolve the engine only from the plugin — $CLAUDE_PLUGIN_ROOT, else the newest copy in ~/.claude/plugins/cache/ — and scripts/test/engine-resolution.test.mjs fails CI if any file under those directories reintroduces either resolution shape it matches: the ${CLAUDE_PROJECT_DIR:+…} expansion that shipped, or a direct $CLAUDE_PROJECT_DIR/scripts/conductor.mjs. It is a regression guard against the pattern that appeared, not a proof that no indirection could express the same idea — a report of one that slips past it is welcome.

Automated scanning

This repository runs Semgrep (SAST) and Trivy (filesystem vulnerability scanning) on every push and pull request to main, plus a weekly scheduled run, with results published to the repository's Security tab. main is branch-protected: no direct pushes, required CI status check, required commit signatures, enforced for admins too.

There aren't any published security advisories