@akinet/akidevrule 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +835 -0
  2. package/LICENSE +21 -0
  3. package/README.md +356 -0
  4. package/claude/CLAUDE.md +40 -0
  5. package/claude/agents/aki-challenger.md +38 -0
  6. package/claude/agents/aki-conduct.md +54 -0
  7. package/claude/agents/aki-hands.md +59 -0
  8. package/claude/agents/aki-judge.md +37 -0
  9. package/claude/agents/aki-maker.md +36 -0
  10. package/claude/fragments/settings.akidoc.fragment.json +15 -0
  11. package/claude/hooks/aki-update-check.mjs +160 -0
  12. package/claude/hooks/aki_version_check.mjs +83 -0
  13. package/docs/ref/macos-codesign-tcc.md +59 -0
  14. package/install.mjs +1067 -0
  15. package/install.ps1 +11 -0
  16. package/install.sh +12 -0
  17. package/package.json +52 -0
  18. package/payload/GEMINI.md +147 -0
  19. package/payload/METHOD-audit-flow.md +147 -0
  20. package/payload/METHOD-audit-subtraction.md +67 -0
  21. package/payload/METHOD-audit-zero-trust.md +49 -0
  22. package/payload/METHOD-deep-think.md +172 -0
  23. package/payload/METHOD-proportionality.md +62 -0
  24. package/payload/METHOD-ux-psych.md +60 -0
  25. package/payload/RULE-agent-behavior.md +138 -0
  26. package/payload/RULE-biz.md +51 -0
  27. package/payload/RULE-coding.md +130 -0
  28. package/payload/RULE-content-write.md +54 -0
  29. package/payload/RULE-db-design.md +26 -0
  30. package/payload/RULE-docs.md +144 -0
  31. package/payload/RULE-pattern-core.md +80 -0
  32. package/payload/RULE-release.md +215 -0
  33. package/payload/RULE-seo.md +173 -0
  34. package/payload/RULE-stack-akiNuxtCf.md +179 -0
  35. package/payload/RULE-stack-tauri.md +59 -0
  36. package/payload/RULE-ui-pattern.md +167 -0
  37. package/payload/index.md +91 -0
  38. package/skills/aki-article-writer/SKILL.md +50 -0
  39. package/skills/aki-article-writer/references/article-workflow.md +377 -0
  40. package/skills/akidevsync-notes/SKILL.md +48 -0
  41. package/skills/akidevsync-notes/scripts/notes_cli.py +212 -0
  42. package/skills/akiflow/SKILL.md +221 -0
  43. package/skills/akiflow/references/harness-facts.md +215 -0
  44. package/skills/akiflow/scripts/council-cost.sh +4 -0
  45. package/skills/akiflow/scripts/council-open.sh +4 -0
  46. package/skills/akiflow/scripts/council-read.sh +4 -0
  47. package/skills/akiflow/scripts/council-verify.sh +4 -0
  48. package/skills/akiflow/scripts/council_cost.py +149 -0
  49. package/skills/akiflow/scripts/council_open.py +323 -0
  50. package/skills/akiflow/scripts/council_read.py +148 -0
  51. package/skills/akiflow/scripts/council_verify.py +315 -0
  52. package/skills/akiflow/scripts/scythe.py +307 -0
  53. package/skills/akiflow/scripts/scythe.sh +4 -0
  54. package/skills/akigitcommit/SKILL.md +85 -0
  55. package/skills/akihelp/SKILL.md +47 -0
  56. package/skills/akihtmlreport/SKILL.md +59 -0
  57. package/skills/akilint/SKILL.md +29 -0
  58. package/skills/akirule/SKILL.md +155 -0
  59. package/skills/akiship/SKILL.md +55 -0
  60. package/skills/akithink/SKILL.md +59 -0
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: aki-judge
3
+ description: Judge an artifact against exactly one standard, named at spawn (pattern, proportion, ux, db, seo, release, …). Returns a verdict with evidence, never a fix. Spawn one per standard rather than asking one agent to hold several.
4
+ tools: Read, Grep, Glob
5
+ model: sonnet
6
+ ---
7
+
8
+ # Mandate
9
+
10
+ Judge the artifact you are given against **exactly one standard**, named in your brief. Return a verdict with evidence.
11
+
12
+ One standard per spawn is the whole design. An agent asked to hold three standards at once averages them into a single mild opinion, and the disagreement between them — which is the useful output — disappears silently. If your brief names more than one, judge the first and say the rest need their own seat.
13
+
14
+ Your verdict must be able to go against whoever spawned you. If you find yourself confirming the caller's stated conclusion, check that you would have reached it from the artifact alone.
15
+
16
+ # Rules you must read before working
17
+
18
+ - `~/.aki/akidevrule/RULE-agent-behavior.md` — the behavior floor, `B5` above all: you report, you never fix. No edits, no git state, ever.
19
+ - **The one standard named in your brief** — the rule or method file itself, read in full. Judging from a remembered summary of a rule is the failure this seat exists to prevent.
20
+
21
+ # Receipt — first line of your output, always
22
+
23
+ ```
24
+ [RULES] agent,pattern (brief) | missing: none
25
+ ```
26
+
27
+ If the standard you were told to judge against could not be read, say so under `missing:` and **stop** — do not judge from memory of it. That is a LOAD-fail, and it is a bug in the brief, not in the artifact.
28
+
29
+ # Output contract
30
+
31
+ - The verdict first: does the artifact meet the standard, and if not, at which specific clause.
32
+ - Per finding: the clause address (`pattern.A2`), the `path:line` it fails at, and the quoted fragment. A finding with no location is not actionable and does not ship.
33
+ - Rank by severity. Never pad the list flat to look thorough — a report where everything is medium tells the caller nothing.
34
+ - Say what you did **not** cover. A standard has clauses your scope could not reach; silence about them reads as a pass.
35
+ - No fixes, no diffs, no rewritten code. Scheduling the fix is the caller's decision.
36
+ - You never escalate to the owner. A conflict goes to the caller as `CONFLICT:`, subject to `agent.A3`'s kill-tests.
37
+ - Never state compliance as a citation. `pattern.A2` in your output proves the address was available to you, nothing more (`agent.B2`).
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: aki-maker
3
+ description: Turn an already-made decision into a diff. The only agent permitted to write. Requires the decision, the exact files, and the domain rules named in its brief — it implements, it does not decide.
4
+ tools: Read, Edit, Write, Bash
5
+ model: sonnet
6
+ ---
7
+
8
+ # Mandate
9
+
10
+ Turn a decision that has already been made into a diff. You are the only agent permitted to write, which is exactly why your scope is the narrowest: you implement what the brief specifies, at the sites the brief names.
11
+
12
+ **You do not decide.** If the brief is ambiguous, under-specified, or turns out to be wrong once you read the code, stop and report the gap. Do not resolve it yourself and do not implement a nearby thing that seems better — an unrequested improvement inside a diff is indistinguishable from the requested change at review time, which is how scope creep becomes permanent.
13
+
14
+ Change exactly what was asked. No adjacent refactors, no cleanup, no renames, no new files beyond those specified (`agent.B1`). Report anything worth doing; never do it silently.
15
+
16
+ # Rules you must read before working
17
+
18
+ - `~/.aki/akidevrule/RULE-agent-behavior.md` — the behavior floor: `B1` scope discipline, `B3` what to ask before, `B4` no model-credit trailers in any git artifact, `C3` never hard-wrap a logical line, `C5` temp files only in the scratchpad.
19
+ - `~/.aki/akidevrule/RULE-coding.md` — `B2` (read the flow and its docs before changing code you did not write; confirm the intents you did *not* set out to touch still hold), `B3` (done means verified, by the narrowest tool that settles the doubt), `B4` (the comment budget — fix the name, then delete the comment).
20
+ - `~/.aki/akidevrule/RULE-pattern-core.md` — `C1` is the definition of done at the pattern level.
21
+ - **The domain rules named in your brief** — stack, ui, db, docs, content, whichever apply. You inherit no router; a domain rule not in your brief is a domain rule you do not have, and you must say so rather than improvise it.
22
+
23
+ # Receipt — first line of your output, always
24
+
25
+ ```
26
+ [RULES] agent,coding,pattern,ui (brief) | missing: none
27
+ ```
28
+
29
+ A non-empty `missing:` on a domain rule means you should not have written that part. Report it instead of guessing what the rule probably said.
30
+
31
+ # Output contract
32
+
33
+ - What changed, per file, in one line each — the change, not a narration of the edit.
34
+ - The verification you actually ran, with its output. Never a build or a dev server on your own initiative: those are the owner's call (cost and side effects), and a change whose risk lives only at runtime is reported as **"unverified — needs a runtime check"** with the exact command, never as "Done" (`coding.B3`).
35
+ - Anything the brief specified that you did **not** do, and why. Silence about a skipped item reads as completion.
36
+ - Attestation as checkable fact, not as citation: `files edited: 4 · git mutations: none · scope: only the paths named in the brief`.
@@ -0,0 +1,15 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Read(//~/.aki/akidevrule/**)",
5
+ "Bash(python3 ~/.claude/skills/*)",
6
+ "Bash(python3 ~/.aki/akidevrule/agskills/*)"
7
+ ],
8
+ "additionalDirectories": [
9
+ "~/.aki/akidevrule"
10
+ ]
11
+ },
12
+ "skillOverrides": {
13
+ "akirule": "on"
14
+ }
15
+ }
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env node
2
+ // akidevrule update-check hook (Claude Code SessionStart). Notify-only, fail-silent, never auto-updates — see README.md § Update notifications for the five-state contract this classifies against.
3
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join, dirname } from "node:path";
6
+ import {
7
+ STATE_UNKNOWN,
8
+ STATE_CURRENT,
9
+ STATE_AHEAD,
10
+ classifyState,
11
+ cmpSemver,
12
+ fetchRemoteChangelog,
13
+ localInstallPresent,
14
+ parseChangelogVersion,
15
+ } from "./aki_version_check.mjs";
16
+
17
+ const CHANGELOG_URL_HUMAN = "https://github.com/lacvietanh/akidevrule/blob/master/CHANGELOG.md";
18
+ const REPO_URL = "https://github.com/lacvietanh/akidevrule";
19
+ const UPDATE_CMD = "npx @akinet/akidevrule@latest";
20
+ const THROTTLE_OK_HOURS = 24; // after a definitive result, wait a full day
21
+ const THROTTLE_FAIL_HOURS = 1; // after offline/timeout, retry sooner so a notice is not lost
22
+
23
+ const HOME = homedir();
24
+ const INSTALL_ROOT = join(HOME, ".aki", "akidevrule");
25
+ const LOCAL_CHANGELOG = join(INSTALL_ROOT, "CHANGELOG.md");
26
+ const THROTTLE_FILE = join(HOME, ".claude", "hooks", ".aki-update-check");
27
+
28
+ function silentExit() {
29
+ process.exit(0);
30
+ }
31
+
32
+ /** True if it is time to check again (also when the marker is missing/unreadable). */
33
+ function checkDue() {
34
+ try {
35
+ const due = parseFloat(readFileSync(THROTTLE_FILE, "utf8").trim());
36
+ if (Number.isNaN(due)) return true;
37
+ return Date.now() / 1000 >= due;
38
+ } catch {
39
+ return true;
40
+ }
41
+ }
42
+
43
+ /** Record the next-allowed check time. */
44
+ function deferCheck(hours) {
45
+ try {
46
+ mkdirSync(dirname(THROTTLE_FILE), { recursive: true });
47
+ writeFileSync(THROTTLE_FILE, String(Math.floor(Date.now() / 1000 + hours * 3600)));
48
+ } catch {
49
+ /* ignore */
50
+ }
51
+ }
52
+
53
+ /** Return [[header, [lines]]] for each '## ' section, in file order. */
54
+ function splitEntries(text) {
55
+ const entries = [];
56
+ let cur = null;
57
+ for (const line of text.split("\n")) {
58
+ if (line.startsWith("## ")) {
59
+ cur = [line.slice(3).trim(), [line]];
60
+ entries.push(cur);
61
+ } else if (cur) {
62
+ cur[1].push(line);
63
+ }
64
+ }
65
+ return entries;
66
+ }
67
+
68
+ function emit(banner, context) {
69
+ process.stdout.write(
70
+ JSON.stringify({
71
+ systemMessage: banner,
72
+ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: context },
73
+ suppressOutput: true,
74
+ }) + "\n"
75
+ );
76
+ }
77
+
78
+ function reportMissing() {
79
+ const banner =
80
+ "📦 akidevrule is not installed on this machine\n" +
81
+ ` Install: ${UPDATE_CMD}\n` +
82
+ ` Repo: ${REPO_URL}`;
83
+ const context =
84
+ "The akidevrule shared-rule corpus is not installed on this machine.\n" +
85
+ `To install it, run: ${UPDATE_CMD}`;
86
+ emit(banner, context);
87
+ }
88
+
89
+ /** Released entries strictly newer than localVersion, newest-first, skipping [Unreleased]. */
90
+ function buildDelta(remoteText, localVersion) {
91
+ const deltaLines = [];
92
+ for (const [header, lines] of splitEntries(remoteText)) {
93
+ const m = header.match(/^\[(\d+\.\d+\.\d+)\]/);
94
+ if (!m) continue; // [Unreleased] or any other non-version heading
95
+ if (cmpSemver(m[1], localVersion) <= 0) break; // reached local's own version or older
96
+ deltaLines.push(...lines);
97
+ }
98
+ return deltaLines.join("\n").trim();
99
+ }
100
+
101
+ function reportUpdate(localVersion, remoteVersion, remoteText) {
102
+ let delta = buildDelta(remoteText, localVersion);
103
+ const maxDelta = 1400;
104
+ if (delta.length > maxDelta) {
105
+ delta = delta.slice(0, maxDelta).replace(/\s+$/, "") + "\n… (see full changelog at the link below)";
106
+ }
107
+ const banner =
108
+ "📢 akidevrule has a new update available\n" +
109
+ ` ${localVersion} → ${remoteVersion}\n` +
110
+ ` Update: ${UPDATE_CMD}\n` +
111
+ ` Changelog: ${CHANGELOG_URL_HUMAN}\n\n` +
112
+ delta;
113
+ const context =
114
+ "The akidevrule shared-rule corpus has a newer version available.\n" +
115
+ `Installed: ${localVersion}. Latest: ${remoteVersion}.\n` +
116
+ `To update, run: ${UPDATE_CMD}\n\n` +
117
+ "What's new (from CHANGELOG.md):\n" +
118
+ delta;
119
+ emit(banner, context);
120
+ }
121
+
122
+ async function main() {
123
+ // Local file-stat, always run and never throttled — a not-installed machine must hear about it every session, not once per THROTTLE_OK_HOURS.
124
+ if (!localInstallPresent(INSTALL_ROOT)) {
125
+ reportMissing();
126
+ silentExit();
127
+ }
128
+
129
+ if (!checkDue()) silentExit();
130
+
131
+ let localText;
132
+ try {
133
+ localText = readFileSync(LOCAL_CHANGELOG, "utf8");
134
+ } catch {
135
+ deferCheck(THROTTLE_OK_HOURS);
136
+ silentExit();
137
+ }
138
+ const localVersion = parseChangelogVersion(localText);
139
+
140
+ const remoteText = await fetchRemoteChangelog();
141
+ const remoteVersion = remoteText ? parseChangelogVersion(remoteText) : null;
142
+
143
+ const state = classifyState(true, localVersion, remoteVersion);
144
+
145
+ if (state === STATE_UNKNOWN) {
146
+ deferCheck(THROTTLE_FAIL_HOURS); // offline/timeout -> retry soon
147
+ silentExit();
148
+ }
149
+
150
+ deferCheck(THROTTLE_OK_HOURS); // definitive answer -> next check in a day
151
+
152
+ if (state === STATE_CURRENT || state === STATE_AHEAD) {
153
+ silentExit(); // up to date, or a dev machine ahead of remote — never nag
154
+ }
155
+
156
+ reportUpdate(localVersion, remoteVersion, remoteText);
157
+ silentExit();
158
+ }
159
+
160
+ main().catch(silentExit);
@@ -0,0 +1,83 @@
1
+ // Shared version-status parser for akidevrule (pattern.A1 SSOT).
2
+ //
3
+ // Imported by both install.mjs (--check / inspect) and aki-update-check.mjs
4
+ // (SessionStart notify hook), so the two never classify the same CHANGELOG
5
+ // differently. A keep-a-changelog file's *latest released* version is its first
6
+ // `## [x.y.z]` heading — the `[Unreleased]` buffer at the top (release.A5's
7
+ // normal working state) is never a version and must be skipped.
8
+ import { statSync } from "node:fs";
9
+ import { join } from "node:path";
10
+
11
+ export const REMOTE_CHANGELOG_URL =
12
+ "https://raw.githubusercontent.com/lacvietanh/akidevrule/master/CHANGELOG.md";
13
+ export const NETWORK_TIMEOUT_MS = 3000;
14
+
15
+ export const STATE_MISSING = "missing";
16
+ export const STATE_CURRENT = "current";
17
+ export const STATE_UPDATE = "update";
18
+ export const STATE_AHEAD = "ahead";
19
+ export const STATE_UNKNOWN = "unknown";
20
+
21
+ const VERSION_HEADING_RE = /^##\s*\[(\d+\.\d+\.\d+)\]/m;
22
+
23
+ /** First released `## [x.y.z]` heading, skipping `[Unreleased]`. null if absent/unparseable. */
24
+ export function parseChangelogVersion(text) {
25
+ if (!text) return null;
26
+ const m = text.match(VERSION_HEADING_RE);
27
+ return m ? m[1] : null;
28
+ }
29
+
30
+ /** -1/0/1 for a<b / a==b / a>b on 3-part numeric semver. Either side missing -> 0 (no claim). */
31
+ export function cmpSemver(a, b) {
32
+ if (!a || !b) return 0;
33
+ const pa = a.split(".");
34
+ const pb = b.split(".");
35
+ for (let i = 0; i < 3; i++) {
36
+ const x = i < pa.length ? parseInt(pa[i], 10) : 0;
37
+ const y = i < pb.length ? parseInt(pb[i], 10) : 0;
38
+ if (x !== y) return x < y ? -1 : 1;
39
+ }
40
+ return 0;
41
+ }
42
+
43
+ /** Raw text of the public CHANGELOG.md. null on any network/HTTP failure — never throws. */
44
+ export async function fetchRemoteChangelog(timeoutMs = NETWORK_TIMEOUT_MS) {
45
+ const controller = new AbortController();
46
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
47
+ try {
48
+ const resp = await fetch(REMOTE_CHANGELOG_URL, {
49
+ headers: { "User-Agent": "aki-update-check" },
50
+ signal: controller.signal,
51
+ });
52
+ if (!resp.ok) return null;
53
+ return await resp.text();
54
+ } catch {
55
+ return null;
56
+ } finally {
57
+ clearTimeout(timer);
58
+ }
59
+ }
60
+
61
+ function isFile(p) {
62
+ try {
63
+ return statSync(p).isFile();
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+
69
+ /** True when installRoot looks intact enough to compare (CHANGELOG.md and index.md both present). */
70
+ export function localInstallPresent(installRoot) {
71
+ return isFile(join(installRoot, "CHANGELOG.md")) && isFile(join(installRoot, "index.md"));
72
+ }
73
+
74
+ /** One of the STATE_* constants — see README.md "Update notifications" for the full 5-state table. */
75
+ export function classifyState(localPresent, localVersion, remoteVersion) {
76
+ if (!localPresent) return STATE_MISSING;
77
+ if (remoteVersion == null) return STATE_UNKNOWN;
78
+ if (localVersion == null) return STATE_AHEAD;
79
+ const cmp = cmpSemver(localVersion, remoteVersion);
80
+ if (cmp === 0) return STATE_CURRENT;
81
+ if (cmp < 0) return STATE_UPDATE;
82
+ return STATE_AHEAD;
83
+ }
@@ -0,0 +1,59 @@
1
+ # Local macOS codesign and TCC
2
+
3
+ The one AkiDevRule lookup for this chain. `tauri.B7` is the concise rule (do not delete it); the installer (`install.mjs`) deploys this file to `~/.aki/akidevrule/docs/ref/macos-codesign-tcc.md`. Evidence trail: `docs/research/macos-tcc-tauri-boundary-aug21.md`.
4
+
5
+ **Stable self-signed identity keeps TCC grants across rebuilds. Apple ad-hoc (`codesign --sign -`) does not.** Artifact names, install paths, and identity names live in that project’s own docs — not here.
6
+
7
+ ## Responsible process
8
+
9
+ TCC judges a child (`git`/`rsync`/`ssh`/a CLI spawned by `std::process::Command`, `tauri-plugin-shell`, a PTY) by the **responsible process** at the head of the chain, inherited across `fork`/`posix_spawn`. Inside a shipped `.app` that is the bundle, never the user’s Terminal. Symptom: the same sidecar works in Terminal and is denied (`EPERM`, or a consent dialog naming the app) only in the bundle.
10
+
11
+ ## Three switches (not peers)
12
+
13
+ - **Full Disk Access** (`kTCCServiceSystemPolicyAllFiles`) — top of the chain, a superset. If the responsible app holds it, protected locations are readable with no per-folder prompt. Request only when reach is genuinely unbounded.
14
+ - **Files & Folders** (`kTCCServiceSystemPolicyDocumentsFolder`, `…DesktopFolder`, `…DownloadsFolder`, plus removable/network volumes) — least privilege; consulted only when there is no FDA. A refusal is **sticky**: one “Don’t Allow” = denial with no re-prompt. Recovery is `tccutil reset` or **removing** the app’s entry — flipping the toggle back on restores access but not prompting.
15
+ - **Developer Tools** (`kTCCServiceDeveloperTool`) — **not file access.** Gatekeeper exemption for software the app *runs* (unsigned/ad-hoc sidecars, local toolchains). It silences no Files & Folders dialog.
16
+
17
+ Scope every spawn (`cwd`, workspace, app data dir). An unbounded walk from `$HOME` is a prompt/`EPERM` storm charged to the bundle.
18
+
19
+ This chain governs consent-based **reads**. User-picked Open/Save or drag-and-drop paths, and file *writes*, are a different diagnosis.
20
+
21
+ When the switch is unknown: `log show --predicate 'subsystem == "com.apple.TCC"' --last 5m` prints `AttributionChain` and the result.
22
+
23
+ ## What TCC stores
24
+
25
+ TCC does not store “this app path is allowed”. It stores a **code signing requirement** (`csreq`). At check time it evaluates the running binary against that requirement.
26
+
27
+ | How the binary is signed | Designated requirement (typical) | Rebuild |
28
+ |---|---|---|
29
+ | Ad-hoc (`-`) | pinned to that build’s **CDHash** | CDHash changes → stored `csreq` fails → grant gone (Settings toggle can still look ON) |
30
+ | Named cert (self-signed or Developer ID) | `identifier "bundle.id" and certificate leaf = H"…"` | CDHash still changes; **leaf hash does not** → grant holds |
31
+
32
+ Measured 2026-08-28 on a Tauri `.app` re-signed with a login-keychain identity: DR was `identifier "<bundle-id>" and certificate leaf = H"…"`. CDHash is expected to change next compile.
33
+
34
+ Do **not** regenerate the cert. A new leaf hash is a new DR; every existing grant dies. Create once; reuse.
35
+
36
+ `security find-identity -p codesigning -v` lists only **trusted** identities. A self-signed cert often shows `CSSMERR_TP_NOT_TRUSTED` and vanishes from `-v`. `codesign --sign "Name"` still works. Probe without `-v`.
37
+
38
+ `sudo codesign` is wrong: root’s keychain is not the login keychain. If the installed `.app` is root-owned from an old `sudo cp`, `chown` once, then sign as the user.
39
+
40
+ ## Gatekeeper is not TCC
41
+
42
+ | | Trigger | Survives rebuild? |
43
+ |---|---|---|
44
+ | **Gatekeeper** | `com.apple.quarantine` (downloads). Local compile typically has none | N/A if never quarantined |
45
+ | **TCC** | first access to a protected resource | yes iff DR stays the same cert+identifier |
46
+
47
+ `xattr -cr` on the installed `.app` strips quarantine if something set it. It does not reset TCC. A “right-click Open once” warning is a Gatekeeper first-launch, not a TCC grant.
48
+
49
+ ## Rebuild and designated requirement
50
+
51
+ A Tauri bundle step ad-hoc-signs. Overwriting that signature with the **same** named identity is required every build; that is the mechanism, not wasted work. `codesign --force --sign "…"` must preserve entitlements. Skip `codesign` after a bundle and you ship ad-hoc again.
52
+
53
+ TCC evaluates the **running** binary against the stored `csreq`. The signed artifact and the `.app` the user launches must be the same path with the same DR. Signing a build output then opening a leftover copy does not keep the grant.
54
+
55
+ `tccutil reset` only clears stale state; it does not prevent the next loss. Install location and identity display name live in that project’s docs.
56
+
57
+ ## Sources
58
+
59
+ [Apple TN3127 — Inside Code Signing: Requirements](https://developer.apple.com/documentation/technotes/tn3127-inside-code-signing-requirements) · [Nick Liu — TCC pins Accessibility to a cdhash](https://www.nick-liu.com/posts/tcc-cdhash-trap/) · [Eclectic Light — app first run / quarantine](https://eclecticlight.co/2020/01/27/what-could-possibly-go-wrong-on-an-app-first-run/) · `tauri.B7` · `docs/research/macos-tcc-tauri-boundary-aug21.md` §5 + §5b