-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocs.html
More file actions
2781 lines (2695 loc) Β· 203 KB
/
Copy pathdocs.html
File metadata and controls
2781 lines (2695 loc) Β· 203 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Termpolis Documentation β the multi-agent AI terminal with shared learning memory</title>
<meta
name="description"
content="Complete guide to Termpolis: the shared memory brain that learns across Claude, Codex, Gemini, and Qwen, Second Opinion cross-agent review, multi-agent swarm orchestration, the MCP server, the AI Security Center, and live observability."
>
<meta name="author" content="Termpolis">
<link rel="canonical" href="https://termpolis.com/docs.html">
<link rel="icon" type="image/svg+xml" href="logo-termpolis-titlebar.svg">
<link rel="shortcut icon" href="logo-termpolis-titlebar.svg">
<!-- Open Graph -->
<meta property="og:type" content="article">
<meta property="og:site_name" content="Termpolis">
<meta property="og:url" content="https://termpolis.com/docs.html">
<meta property="og:title" content="Termpolis Documentation β the multi-agent AI terminal with shared learning memory">
<meta property="og:description" content="Every feature, panel, and keyboard shortcut in Termpolis β the terminal where Claude, Codex, Gemini, and Qwen work as a team.">
<meta property="og:image" content="https://termpolis.com/assets/screenshot-welcome.png">
<meta property="og:image:width" content="1368">
<meta property="og:image:height" content="908">
<meta property="og:image:alt" content="Termpolis welcome screen with AI agents, workspaces, and swarm controls.">
<meta property="og:locale" content="en_US">
<!-- Twitter / X -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Termpolis Documentation β Multi-Agent AI Terminal Guide">
<meta name="twitter:description" content="Every feature, panel, and shortcut in the AI terminal where Claude, Codex, Gemini, and Qwen work as a team.">
<meta name="twitter:image" content="https://termpolis.com/assets/screenshot-welcome.png">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="docs.css?v=20260705">
<!-- Cloudflare Web Analytics -->
<script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "69e1842746294b5bbb72378c748cb522"}'></script>
<!-- End Cloudflare Web Analytics -->
</head>
<body class="docs-body">
<div class="docs-shell">
<header class="docs-header">
<a class="brand" href="index.html" aria-label="Termpolis home">
<img class="brand-logo" src="logo-termpolis.svg" alt="Termpolis logo">
<span class="brand-text">Termpolis</span>
</a>
<nav class="site-nav" aria-label="Primary">
<a href="index.html#security">Security</a>
<a href="index.html#swarm">Smart Swarm</a>
<a href="index.html#observability">Observability</a>
<a href="index.html#share">Share to Slack/Teams</a>
<a href="index.html#features">Features</a>
<a href="index.html#downloads">Downloads</a>
<a href="docs.html" aria-current="page" class="active">Docs</a>
</nav>
<div class="docs-search">
<input
id="docs-search-input"
type="search"
placeholder="Search docsβ¦"
aria-label="Search documentation"
autocomplete="off"
>
</div>
</header>
<div class="docs-layout">
<aside class="docs-sidebar" aria-label="Documentation sections">
<div class="docs-sidebar-inner">
<p class="docs-sidebar-eyebrow">Get started</p>
<ul class="docs-toc">
<li><a href="#overview" class="docs-toc-link">Overview</a></li>
<li><a href="#install">Installation</a></li>
<li><a href="#install-windows-defender" class="docs-toc-sub">β³ Windows Defender FP recovery</a></li>
<li><a href="#api-keys">Before you begin Β· API keys</a></li>
<li><a href="#first-launch">First launch (5 steps)</a></li>
<li><a href="#first-agent">Launch your first AI agent</a></li>
<li><a href="#swarm-vs-single">Swarm vs. single agent</a></li>
</ul>
<p class="docs-sidebar-eyebrow">π‘ Security</p>
<ul class="docs-toc">
<li><a href="#security-center">AI Security Center</a></li>
<li><a href="#security-secret-scanner">Prompt secret watch</a></li>
<li><a href="#security-commit-shield">Commit & push Secret Shield</a></li>
<li><a href="#security-commit-hooks" class="docs-toc-sub">β³ Protect a repository (git hooks)</a></li>
<li><a href="#security-sensitive-file">Sensitive-file-read alert</a></li>
<li><a href="#security-codechunk">Code-chunk + env-dump heuristics</a></li>
<li><a href="#security-egress">Per-agent egress audit</a></li>
<li><a href="#security-egress-guard">Egress Guard (allowlist policy)</a></li>
<li><a href="#security-memory-scrub">Memory-at-rest secret scrub</a></li>
<li><a href="#security-tos-drift">ToS drift watcher</a></li>
<li><a href="#security-gemini">Gemini account-mode + Strict Mode</a></li>
<li><a href="#security-agent-command">Agent command enforcement</a></li>
<li><a href="#security-audit">Audit log</a></li>
<li><a href="#security-disclaimer">Legal disclaimer</a></li>
<li><a href="#safe-import">Safe Import (skills & plugins)</a></li>
</ul>
<p class="docs-sidebar-eyebrow">Share output</p>
<ul class="docs-toc">
<li><a href="#copy-share">Copy for Slack / Teams / PRs</a></li>
</ul>
<p class="docs-sidebar-eyebrow">Core terminal</p>
<ul class="docs-toc">
<li><a href="#sidebar">Sidebar</a></li>
<li><a href="#workspaces">Workspaces</a></li>
<li><a href="#terminals">Terminals</a></li>
<li><a href="#views">Tabs, splits & grid</a></li>
<li><a href="#settings">Settings</a></li>
<li><a href="#themes">Themes</a></li>
<li><a href="#keybindings">Keybindings</a></li>
<li><a href="#voice">Voice dictation</a></li>
</ul>
<p class="docs-sidebar-eyebrow">Productivity</p>
<ul class="docs-toc">
<li><a href="#command-palette">Command palette</a></li>
<li><a href="#prompt-templates">Prompt templates</a></li>
<li><a href="#workflows">Workflow templates</a></li>
<li><a href="#context">Context panel</a></li>
<li><a href="#context-handoff">Cross-AI context handoff</a></li>
<li><a href="#history">History search</a></li>
<li><a href="#conversation">Conversation search</a></li>
<li><a href="#git">Git panel</a></li>
</ul>
<p class="docs-sidebar-eyebrow">AI & swarm</p>
<ul class="docs-toc">
<li><a href="#agents">AI agent profiles</a></li>
<li><a href="#second-opinion">Second Opinion</a></li>
<li><a href="#capabilities">Capability ratings</a></li>
<li><a href="#mcp">MCP server</a></li>
<li><a href="#swarm">Swarm dashboard</a></li>
<li><a href="#conductor">AI conductor</a></li>
<li><a href="#activity">Activity feed</a></li>
<li><a href="#intervention">Intervention controls</a></li>
<li><a href="#review">Swarm review</a></li>
<li><a href="#memory">Shared memory</a></li>
<li><a href="#vector-memory" class="docs-toc-sub">β³ Vector memory & int8</a></li>
<li><a href="#weave">The Weave</a></li>
<li><a href="#code-graph">Code graph</a></li>
<li><a href="#observability-docs">Observability</a></li>
</ul>
<p class="docs-sidebar-eyebrow">Reference</p>
<ul class="docs-toc">
<li><a href="#status-bar">Status bar</a></li>
<li><a href="#shortcuts">Keyboard shortcuts</a></li>
<li><a href="#architecture">Architecture</a></li>
<li><a href="#troubleshooting">Troubleshooting</a></li>
</ul>
<p class="docs-sidebar-eyebrow">Help us improve</p>
<ul class="docs-toc docs-toc-external">
<li>
<a
class="docs-sidebar-bug"
href="https://github.com/codedev-david/termpolis/issues/new?template=bug_report.md"
target="_blank"
rel="noreferrer"
>π Report a bug</a>
</li>
<li>
<a
href="https://github.com/codedev-david/termpolis/issues/new?template=feature_request.md"
target="_blank"
rel="noreferrer"
>β¨ Request a feature</a>
</li>
<li>
<a
href="https://github.com/codedev-david/termpolis/issues"
target="_blank"
rel="noreferrer"
>π¬ Browse all issues β</a>
</li>
</ul>
</div>
</aside>
<main class="docs-content" id="docs-content">
<article>
<header class="docs-hero">
<p class="eyebrow">Documentation</p>
<h1>Termpolis Docs</h1>
<p class="docs-lead">
The definitive guide to Termpolis β <strong>Secure AI-Assisted Development</strong>.
The local-first multi-agent terminal where Claude, Codex, Gemini, and Qwen work as a
team without your source code leaving the machine. Built-in
<a href="#security-center">AI Security Center</a>, swarm orchestration, MCP server,
and a one-shortcut path from terminal to Slack, Teams, or a PR.
Every feature, every panel, every shortcut.
</p>
<p class="docs-hero-actions">
<a
class="docs-bug-btn"
href="https://github.com/codedev-david/termpolis/issues/new?template=bug_report.md"
target="_blank"
rel="noreferrer"
>π Submit a bug report</a>
<a
class="docs-feature-btn"
href="https://github.com/codedev-david/termpolis/issues/new?template=feature_request.md"
target="_blank"
rel="noreferrer"
>β¨ Request a feature</a>
</p>
</header>
<!-- OVERVIEW -->
<section id="overview" class="docs-section">
<h2>Overview</h2>
<figure class="docs-shot">
<img src="docs/screenshots/01-welcome-screen.png" alt="Termpolis welcome screen with AI agents and swarm.">
<figcaption>The welcome screen, where new sessions begin.</figcaption>
</figure>
<p>
Termpolis is a cross-platform desktop terminal manager (Windows, macOS, Linux) built on
Electron + React + TypeScript with <code>node-pty</code> powering the underlying shells.
It ships as a native app, code signed on Windows, notarized on macOS.
</p>
<p><strong>What makes it different:</strong></p>
<ul class="docs-list">
<li><strong>Secure AI-assisted development.</strong> Built-in <a href="#security-center">AI Security Center</a> <a href="#security-secret-scanner">watches every AI prompt against 97 secret rules and names what leaked</a>, <a href="#security-commit-shield">blocks the commit or push that would leak one</a>, <a href="#safe-import">statically vets any third-party skill, plugin, or MCP server before it can be installed</a>, enforces Gemini paid-tier mode, and keeps an auditable JSONL log β every check runs locally.</li>
<li><strong>Multi-agent swarm.</strong> Claude Code, Codex, Gemini CLI, and Qwen Code work together on a task. A dedicated Claude Code instance acts as the conductor.</li>
<li><strong>MCP server baked in.</strong> AI agents control Termpolis via Model Context Protocol, open terminals, run commands, send messages.</li>
<li><strong>Transparent routing.</strong> Every subtask shows which agent got it, why, and what it cost.</li>
<li><strong>Activity observability.</strong> Every token, every tool call, every message from every agent is visible in real time.</li>
<li><strong>Intervention controls.</strong> Pause, cancel, or steer any agent mid-task without leaving the feed.</li>
<li><strong>Shared memory.</strong> A RAG-backed memory store that any agent can read and write via MCP.</li>
<li><strong>MCP-native end to end.</strong> All four agents speak MCP β no terminal-output parsers, no glue scripts, no bridge code paths.</li>
<li><strong>Share-ready output.</strong> One shortcut from terminal to Slack, Teams, or a PR β see <a href="#copy-share">Copy for Slack / Teams / PRs</a>.</li>
</ul>
</section>
<!-- AI SECURITY CENTER -->
<section id="security-center" class="docs-section">
<h2>π‘ AI Security Center</h2>
<p>
Termpolis ships an in-app AI Security Center at <strong>Settings β AI Security</strong>.
Its goal is to give administrators visibility and layered controls around outbound
AI traffic so a team can adopt Claude Code, Codex, Gemini CLI, and Qwen Code with
the obvious accidents caught and a verifiable record of what was sent β knowing
that no in-the-loop tool can guarantee a hosted-model prompt never reaches the
provider.
</p>
<p>
<strong>What it is and isn't.</strong> Any tool that lets you talk to a hosted model
is, by definition, sending your prompt to that provider. Termpolis cannot air-gap
a prompt you choose to send and cannot guarantee a provider's stated retention
policy is enforced server-side. The Security Center is <em>defense in depth</em> β
it catches recognisable secrets, flags oversize code/<code>.env</code> pastes,
detects unexpected network endpoints, and writes everything to a local audit log.
For absolute air-gap, run a local model and accept the quality / hardware
trade-off.
</p>
<p><strong>Design principles:</strong></p>
<ul class="docs-list">
<li><strong>Local-first.</strong> Every check runs on the machine. None of these features send data to Termpolis or any third party.</li>
<li><strong>Native, not a browser/IDE plugin.</strong> Termpolis is a terminal manager, not a Chrome extension piping your buffer to a SaaS backend.</li>
<li><strong>No telemetry.</strong> No login, no phone-home. Optional crash reporting is opt-in and redacts user-folder paths first.</li>
<li><strong>Verifiable.</strong> Every claim links to the provider's published ToS page; a weekly drift watcher (v1.11.52) opens a tracking issue when those pages change.</li>
<li><strong>Honest about limits.</strong> Each control below names what it can <em>not</em> catch.</li>
</ul>
<h3>Per-provider training-disposition facts</h3>
<p>
The panel summarizes how each provider treats prompts on their commercial tier, sourced
from the official ToS pages and updated with each release of Termpolis:
</p>
<ul class="docs-list">
<li><strong>Anthropic (Claude Code).</strong> API + commercial usage β default off for training.</li>
<li><strong>OpenAI (Codex).</strong> API platform β default off for training.</li>
<li><strong>Google (Gemini CLI).</strong> Paid tier (API key, Vertex AI, Code Assist) β excluded from training. Free OAuth tier β Google may use prompts to improve products. Flagged yellow.</li>
<li><strong>Alibaba (Qwen Code).</strong> Paid DashScope β excluded from training. Local Ollama mode β prompts stay on your machine instead of going to any provider.</li>
</ul>
<h3 id="security-secret-scanner">Watch every prompt (v1.25.2)</h3>
<p>
Once you launch <code>claude</code>, <code>codex</code>, <code>gemini</code>, or
<code>qwen</code> in a terminal, every Enter and every paste-sized chunk
(β₯32 bytes) is scanned in main-process memory against <strong>97 secret rules</strong>.
The scan runs on a <strong>shadow copy</strong>. Every byte you type is forwarded to the
agent <strong>immediately and unmodified</strong> β nothing is withheld, nothing is
rewritten β and the scan never runs per keystroke, only on submit or paste
(~0.05 ms per prompt). Non-AI terminals are not scanned at all. It cannot be turned
off. Coverage spans:
</p>
<ul class="docs-list">
<li>AWS (access keys <code>AKIAβ¦</code>/<code>ASIAβ¦</code>, secrets, session tokens).</li>
<li>GitHub (PAT <code>ghp_β¦</code>, fine-grained <code>github_pat_β¦</code>, OAuth secrets, runner tokens), GitLab, Bitbucket.</li>
<li>Azure (Storage AccountKey, SAS signature, connection strings, AD client secret, DevOps PAT) and GCP (service-account JSON, OAuth client ID).</li>
<li>AI providers: OpenAI, Anthropic, Google, HuggingFace, Cohere, Replicate.</li>
<li>Payments (Stripe, PayPal Braintree, Square), comms (Slack, Discord, Telegram, Twilio, SendGrid, Mailgun, Mailchimp, Postmark).</li>
<li>Cloud (Cloudflare, DigitalOcean, Heroku, Netlify, Vercel, Fly.io, Render, Pulumi), CI/CD (CircleCI, Travis, Codecov).</li>
<li>Observability (Sentry DSN, Datadog, New Relic, Rollbar, Honeycomb, Mapbox, Okta, Auth0).</li>
<li>Project mgmt (Linear, Notion, Asana, Jira, Figma), package registries (npm, PyPI, Docker Hub).</li>
<li>Secrets vaults (HashiCorp Vault, Doppler, 1Password Connect), DB connection strings (Postgres/MySQL/MongoDB/Redis), HTTP basic-auth URLs.</li>
<li>JWT, PEM/GPG private key blocks, and the <code>.env</code>-style catch-all (<code>SECRET_KEY=β¦</code>).</li>
<li><strong>Named secrets in config-shaped text</strong> β <code>.env</code>,
<code>appsettings.json</code>, YAML, connection strings, and URLs carrying credentials.
The audit entry reports the <em>name</em> (<code>DB_PASSWORD</code>), never the value.</li>
<li><strong>Shapeless secrets you introduce in your own words</strong> β <em>"here is the
api key for this code, add it to line 42: 8f3a9b2c4d5eβ¦"</em>. No token-shape rule could
ever see that one. It stays quiet on ordinary talk like <em>"please rotate the api key
in production"</em>.</li>
</ul>
<p>
Known token shapes (AWS, OpenAI, GitHub, Google, Stripe, JWT) are caught
<strong>bare, in prose, with no name needed</strong>. A manual paste-and-scan box also
lives in Settings β AI Security for one-off checks of clipboard text before pasting
elsewhere.
</p>
<p>
<strong>A hit is recorded, not blocked.</strong> It is written to the
<a href="#security-audit">audit log</a> as a <code>prompt_secret_sent</code> event that
names <em>what</em> leaked β <code>DB_PASSWORD (env_secret)</code> β and
<strong>never the value</strong>. The secret itself is never captured and never written
to disk, not even a fragment. The name is the part you can act on: it is what tells you
which credential to rotate.
</p>
<p>
<strong>Why this is detection and not prevention.</strong> Through v1.25.1 the app claimed
to <em>redact</em> a secret before it reached the PTY. That has been removed, because it
could never have worked. To redact before the PTY you must withhold the keystrokes β which
broke typing outright β and even a working version would buy you nothing: against a TUI
agent like Claude Code, your text is already sitting in the <em>agent's own</em> line
buffer by the time you press Enter, so writing a "clean" copy to the PTY would only
<em>append</em> to it. You cannot un-send what the agent already holds. So the prompt path
is <strong>detection only, by design</strong>, and saying so plainly is better than the old
claim, which was false. Prevention lives at the two boundaries where it is actually
possible: the <a href="#security-commit-shield">git boundary</a>, where a commit or push
carrying a key is genuinely blocked, and the
<a href="#security-memory-scrub">memory layer</a>, where a secret is stripped before it can
ever be written to the brain.
</p>
<p>
Dropping redaction is also what made the rule set <em>bigger</em>. A false positive used to
<strong>mangle your prompt</strong>; now it costs one line in a log. That is what made the
7 name-aware and narrative rules above safe enough to ship.
</p>
<p>
Be clear-eyed about the limit. This does <strong>not</strong> stop a secret reaching a
model through a prompt, and it is <strong>not</strong> a comprehensive DLP solution. A
bespoke corporate token that nobody publishes a shape for, pasted with no name attached,
can still go unnoticed and must be vetted separately.
</p>
<h3 id="security-commit-shield">Commit & push Secret Shield (v1.25)</h3>
<p>
The watch above only ever sees text on its way <em>to an agent</em> β and there, all it
can do is record. It never saw <code>git</code>. So a key that an agent wrote into a file
β or one you pasted into a config and forgot about β could still be committed, land in
your history, and be pushed to a remote without a single check firing. That was the hole.
<strong>Git is a boundary Termpolis can actually hold</strong>, so this is where prevention
lives: the Secret Shield runs the same secret-rule engine at the
<strong>git boundary</strong>:
</p>
<ul class="docs-list">
<li><strong>On commit</strong> β it scans the <strong>staged diff</strong>
(<code>git diff --cached</code>), which is precisely what the commit is about to
capture.</li>
<li><strong>On push</strong> β it scans <strong>every unpushed commit patch</strong>,
which is precisely what the push is about to send. So a secret already sitting in local
history is caught before it leaves the machine, not just one you're adding right now.</li>
</ul>
<p>
A hit <strong>blocks the git operation</strong> and tells you <strong>which rule
fired</strong>, so you can strip the value (or rewrite the offending commit) and go again.
It is a hard stop, not a log line: unlike the prompt path β which can only tell you what
has already gone out β the commit simply does not happen. Nothing is silently rewritten
behind your back, and no half-made commit is left behind.
</p>
<h4 id="security-commit-hooks">Covering git you type yourself: the hooks (v1.25.1)</h4>
<p>
Out of the box the shield gates the git operations <em>Termpolis itself runs</em>: the
Git panel and Swarm Review. That leaves the way most people actually commit β
<code>git commit</code> typed into a terminal β going straight past it. So install the
hooks: <strong>Settings → AI Security → Protect a repository</strong> writes a
<code>pre-commit</code> and a <code>pre-push</code> hook into that repo, and the secret
is caught <strong>however you commit</strong> β terminal, IDE, or script.
</p>
<ul class="docs-list">
<li><strong>It works with Termpolis closed.</strong> The hook shells out to a standalone
scanner that carries its own copy of the rule table and needs only Node and git. A hook
that only guarded you while the app was running would silently stop guarding you the
moment you quit β worse than no hook, because you would still believe you had one.</li>
<li><strong>It chains, it does not clobber.</strong> An existing POSIX-shell hook (husky,
lint-staged) is preserved and still runs, and <em>its</em> exit code still gates the
commit. The shield is spliced in as a sentinel-delimited block below the shebang, runs
first, and then <em>falls through</em> to whatever was already there β so uninstalling
gives you back the original file byte for byte.</li>
<li><strong>A non-shell hook is skipped, and that repo stays unprotected.</strong> If the
repo already has a hook written in something other than POSIX shell β the
<code>pre-commit</code> framework generates <em>Python</em> ones β Termpolis
<strong>refuses to touch it</strong> rather than corrupt it by injecting <code>sh</code>
into a Python file. That is the right call, but be clear about what it means: no shield
is installed there. The Protect-a-repository panel is where you find out which repos
actually took the hook.</li>
<li><strong>It fails open.</strong> If Node is missing, or Termpolis has been uninstalled
and the scanner is gone, the hook exits 0. A hook left behind by a deleted app must
never wedge your git.</li>
</ul>
<p>
<strong>Fixed in v1.25.6 — the panel could report <code>PROTECTED</code> when it was
not.</strong> The list of protected repositories was compared with a bare string
<code>!==</code>. But an install stores either the path the renderer supplied (forward
slashes) or the one the native folder picker returned (OS-native, backslashes on
Windows) — so installing a repo <em>via the picker</em> and then uninstalling it
<em>from the working directory</em> never matched. The repo stayed in
<code>commit-shield-repos.json</code> and the panel kept listing it as protected
<strong>after its hooks had been removed</strong>. The entry is now keyed on a canonical
path (resolved, separators unified and case-folded on Windows only — on macOS and
Linux a backslash is a legal filename character and the filesystem is case-sensitive, so
folding either would conflate two genuinely different repositories). That also stops one
repo being stored twice under two spellings. <em>A security control that claims to be
armed when it is not is worse than one that admits it is off.</em>
</p>
<div class="docs-callout">
<strong>Be clear-eyed about the limit.</strong> <code>git commit --no-verify</code>
bypasses <em>any</em> git hook β that is git’s design, and it is your machine.
This is a strong net, not a cage. It is here to stop the accident, not a determined
author.
</div>
<p>
<strong>It fails open.</strong> If <code>git</code> itself errors β not a repository, no
upstream, git off the PATH β the shield gets out of the way and lets the operation
proceed. It can block you for a <em>secret</em> and nothing else; it will never wedge a
commit for a reason that has nothing to do with secrets.
</p>
<p>
<strong>On by default</strong> β toggle it under <strong>Settings β AI Security β
Commit Shield</strong>. It inherits the rule engine's limits: these are high-confidence
patterns, so a bespoke corporate token shape may not be recognised and still needs its
own review.
</p>
<h3 id="security-gemini">Gemini account-mode auto-detect + Strict Mode</h3>
<p>
The Gemini CLI is the highest-risk surface, because the free OAuth tier may be used
for product improvement. Termpolis inspects the running shell environment to identify
which tier the CLI will hit:
</p>
<ul class="docs-list">
<li><strong>Vertex AI</strong> β <code>GOOGLE_APPLICATION_CREDENTIALS</code> + <code>GOOGLE_CLOUD_PROJECT</code>.</li>
<li><strong>Code Assist (Workspace)</strong> β <code>GOOGLE_GENAI_USE_GCA=true</code>.</li>
<li><strong>Paid API key</strong> β <code>GEMINI_API_KEY</code> or <code>GOOGLE_API_KEY</code>.</li>
<li><strong>Free OAuth fallback</strong> β none of the above. Flagged in red.</li>
</ul>
<p>
When <strong>Strict Mode</strong> is enabled, Termpolis intercepts <code>gemini</code>
invocations from any terminal. If the resolved account mode isn't paid-tier-safe, the
launch is cancelled with <kbd>Ctrl+C</kbd>, an in-band ANSI banner explains why, and the
<code>gemini</code> text <strong>never reaches the PTY</strong>. The blocked launch is
recorded in the audit log as <em>BLOCKED: strict-mode + free-tier</em>. So unlike the
prompt watch β which never withholds anything β Strict Mode genuinely does stop the
command from running.
</p>
<div class="docs-callout">
<strong>Strict Mode is the one security control that ships OFF.</strong> The audit log,
the <a href="#security-commit-shield">Commit Shield</a>, the
<a href="#security-egress-guard">Egress Guard</a> and the
<a href="#security-memory-scrub">memory scrub</a> all default to <em>on</em>, and the
<a href="#security-secret-scanner">prompt watch</a> cannot be turned off at all. Strict
Mode is opt-in because it is the only one that can <em>refuse an action you meant to
take</em> β and a control that blocks legitimate work without being asked is a control
people rip out.
</div>
<p>
<strong>Be clear-eyed about the limits.</strong> Detection is an env-var heuristic, so a
<strong>Workspace Code Assist licence that carries no environment variables is
misclassified as free-tier</strong> and will be blocked β the panel says so in place.
Strict Mode also only intercepts shell-level invocations of the <code>gemini</code>
binary: it does not cover out-of-band paths (a renamed binary, a script that calls the
Google API directly, a Gemini session started outside Termpolis). It is a tripwire for
the common-case mistake, not a proxy.
</p>
<h3 id="security-agent-command">Agent command enforcement (swarm)</h3>
<p>
In a swarm, the <a href="#conductor">AI conductor</a> is itself a model β and it is the
thing deciding what commands the other agents run. That is a genuine injection surface: an
agent whose output the conductor reads can try to talk the conductor into launching the
next agent with different flags. So the conductor is not trusted to compose a launch
command.
</p>
<p>
<code>src/main/agentCommandSanitizer.ts</code> intercepts every swarm-launched agent
command and rebuilds it from the <strong>approved base command for that agent</strong>.
Anything the conductor tried to add is dropped:
</p>
<ul class="docs-list">
<li><strong>Unauthorized flags are stripped</strong> β notably <code>-p</code> (headless
print mode) and <code>--sandbox</code>, which change what the agent is allowed to do.</li>
<li><strong>An appended prompt is stripped.</strong> The conductor delegates work through
the swarm's message channel, not by smuggling instructions onto a command line.</li>
<li><strong>Only exact model aliases survive.</strong> <code>--model</code> is checked
against an authoritative allowlist (<code>opus</code>, <code>sonnet</code>,
<code>haiku</code>, <code>fable</code> β Claude only). The value is whitespace-tokenised
and matched <em>exactly</em>, then the command is rebuilt from the trusted base plus
that alias β so a quoted, concatenated, or injected value can never ride along.</li>
</ul>
<p>
<strong>Limit:</strong> this governs the MCP command surface β the launch commands
Termpolis runs on the swarm's behalf. It does not constrain arbitrary subprocesses an
agent spawns for itself once it is running; that is what the
<a href="#security-egress-guard">Egress Guard</a> and the
<a href="#security-sensitive-file">sensitive-file watcher</a> are for.
</p>
<h3 id="security-audit">Local JSONL audit log (on by default since v1.25)</h3>
<p>
Every AI-agent terminal launch is recorded in <code>ai-security-audit.jsonl</code> inside
the Termpolis <a href="#install">data directory</a> (<code>%APPDATA%\termpolis\</code> on
Windows). Each record is one JSON object containing the timestamp, the agent, the terminal
id, and (optionally) byte counts and hit counts. The file is append-only with 10 MB
rotation. It can be wiped from Settings β AI Security at any time.
</p>
<p>
<strong>It now records by default.</strong> Through v1.24 the audit trail defaulted to
<em>off</em> β which meant that for most users the file did not exist at all, and the
evidence you would most want after an incident was never being written in the first place.
As of <strong>v1.25</strong> it records out of the box.
</p>
<p><strong>The event vocabulary</strong> β what you will actually find in the file:</p>
<ul class="docs-list">
<li><code>prompt_secret_sent</code> β the <a href="#security-secret-scanner">prompt
watch</a> matched a credential in something you sent to an agent. Records the
<em>name</em> and rule (<code>DB_PASSWORD (env_secret)</code>), never the value. This is
the one that drives the “Rotate these” panel.</li>
<li><code>code_chunk_sent</code> / <code>env_dump_sent</code> β a large code paste or an
<code>.env</code>-shaped dump went out. <a href="#security-codechunk">Separate events
since v1.25.2</a>, so a big paste no longer masquerades as a leaked key.</li>
<li><code>commit_blocked</code> / <code>push_blocked</code> β the
<a href="#security-commit-shield">Commit Shield</a> stopped a git operation. Unlike the
prompt events, these record something that <em>did not happen</em>.</li>
<li><code>egress_violation</code> β an agent connected to a host outside the
<a href="#security-egress-guard">provider allowlist</a>.</li>
<li><code>sensitive_file_read</code> β an agent read a
<a href="#security-sensitive-file">high-risk file</a> on its own initiative.</li>
<li><code>import_scan</code> / <code>import_blocked</code> β a
<a href="#safe-import">Safe Import</a> verdict and install decision.</li>
<li><code>memory_scrub</code> β a secret was
<a href="#security-memory-scrub">stripped before a memory was written</a>. Rule ids
only.</li>
<li><code>terminal_open</code> β an AI terminal was launched (and, for a
<a href="#security-gemini">Strict Mode</a> refusal, that the launch was blocked).</li>
</ul>
<div class="docs-callout">
<strong>The scan is always on; the recording is not.</strong> The
<a href="#security-secret-scanner">prompt watch</a> cannot be turned off β but the audit
log <em>can</em>. Switch it off and the watch keeps scanning while nothing is written
down, which means you lose the one thing the prompt path can actually give you: the list
of credentials to rotate. The panel says so in place rather than pretending to be armed.
</div>
<p>
<strong>Read it without leaving the app (v1.25.2).</strong> <strong>Settings β AI
Security β "Open the audit log"</strong> opens a viewer over the raw JSONL, led by a
<strong>"Rotate these β they were sent to a model"</strong> panel. That panel lists every
<code>prompt_secret_sent</code> event the
<a href="#security-secret-scanner">prompt watch</a> recorded, <em>by name</em>
(<code>DB_PASSWORD (env_secret)</code>). The value is not shown because it was never
captured β the name is the part you act on, and rotating it is the remedy the prompt path
can actually give you.
</p>
<p>
<strong>Fixed in v1.25.6 — a <code>NaN</code> limit dumped the entire log.</strong>
Reads of the log are capped (a default page of 200, a hard ceiling of 2,000) so a
renderer-side call can never haul the whole file into memory at once. The guard checked
<code>typeof limit === 'number'</code> — which is <code>true</code> for
<code>NaN</code>. So <code>NaN</code> took the clamp arm instead of the default, and
<code>Math.min</code>/<code>Math.max</code> <em>propagate</em> <code>NaN</code> rather
than clamping it; the resulting <code>slice(NaN)</code> degrades to <code>slice(0)</code>
— “return everything”, which is the one thing the cap exists to
prevent, and it was reachable straight from the renderer. The guard is now
<code>Number.isFinite</code>.
</p>
<p>
The audit log captures only what Termpolis observes locally β activity that bypasses
Termpolis (a Gemini CLI run from a separate native terminal window, for example) is
not visible to the audit log.
</p>
<h3 id="security-sensitive-file">Sensitive-file-read alert (v1.11.53)</h3>
<p>
The prompt watch can only see what <em>you</em> type. When the AI
agent autonomously decides to read a sensitive file via its own
<code>Read</code> tool (or runs <code>cat</code>/<code>head</code>/<code>grep</code>
on one through <code>Bash</code>), the file's bytes have already been added to
the agent's context and transmitted to the provider on the next turn. The
terminal-side scanner never sees that path. This watcher closes the gap.
</p>
<p>
<strong>How it works.</strong>
<code>src/main/sensitiveFileWatcher.ts</code> subscribes to
<code>agentEventBus</code> <code>tool_call</code> events emitted by the
transcript watchers (Claude Code / Codex / Gemini). For each event it inspects
the tool name and arguments:
</p>
<ul>
<li>Filesystem-style tools (<code>Read</code>, <code>Edit</code>,
<code>Write</code>, <code>read_file</code>, <code>view</code>, β¦) β
pulls <code>file_path</code> / <code>path</code> / <code>filename</code> from the input.</li>
<li>Shell-style tools (<code>Bash</code>, <code>run_shell_command</code>,
<code>container.exec</code>, β¦) β tokenises the command, splits on
<code>&&</code>/<code>;</code>/<code>|</code>, and identifies positional
arguments to <code>cat</code>/<code>head</code>/<code>tail</code>/<code>grep</code>/<code>cp</code>/<code>curl -F file=@</code> and 23 other reader commands (29 in all, including the
PowerShell <code>Get-Content</code>/<code>Select-String</code> family).</li>
</ul>
<p>
Each candidate path is matched against a hand-curated rule list:
<code>.env</code>/<code>.env.local</code>/<code>.env.production</code>
(excluding <code>.env.example</code>/<code>.sample</code>/<code>.template</code>);
<code>*.pem</code>/<code>*.key</code>/<code>*.p12</code>/<code>*.pfx</code>/<code>*.jks</code>/<code>*.keystore</code>;
<code>id_rsa</code>/<code>id_ed25519</code>/<code>id_ecdsa</code>/<code>id_dsa</code> (excluding <code>.pub</code>);
<code>~/.aws/credentials</code> + <code>~/.aws/config</code>;
GCP service-account JSONs;
files under <code>~/.ssh/</code> excluding <code>known_hosts</code>/<code>config</code>/<code>*.pub</code>;
<code>.netrc</code>/<code>.npmrc</code>/<code>.pypirc</code>;
<code>~/.docker/config.json</code>; <code>~/.kube/config</code> + <code>*.kubeconfig</code>;
Azure credentials; database-URL config;
<code>secrets.{yml,yaml,json,env,toml,ini}</code>; <code>credentials.{yml,json,β¦}</code>;
the <strong>GnuPG private key store</strong> (<code>secring.gpg</code> and
<code>*.key</code> under <code>.gnupg/</code>, excluding the <em>public</em>
<code>pubring.gpg</code>/<code>pubring.kbx</code>);
<code>*.kdbx</code>/<code>*.kdb</code> (KeePass);
and Chrome/Firefox/Edge cookie databases.
</p>
<div class="docs-callout">
<strong>Fixed in v1.25.6 — the GnuPG rule could never fire.</strong>
<code>secring.gpg</code>, your <em>private</em> keyring and the single file that rule
exists to catch, had been grouped into the rule’s own <strong>exclusion</strong>
list alongside the public keyrings. The exclusion returned <code>false</code> before the
match could ever return <code>true</code>, so an agent reading your private GnuPG keyring
was <strong>never flagged</strong> — and the failure mode was total silence, which
reads exactly like “nothing happened”: the worst possible failure mode for a
watcher. Only the <code>pubring.*</code> entries are excluded now, and a test pins it.
</div>
<p>
On a match the watcher writes an audit entry tagged
<code>sensitive_file_read</code> (with rule id, tool name, source path) and
fires a <code>terminal:sensitive-file-read</code> IPC event to the renderer
which displays a banner naming the file and the agent. A per-terminal counter
is exposed via <code>aiSecurity.sensitiveReads(terminalId)</code> so the
Security panel can show "3 sensitive reads this session" alongside the running
list.
</p>
<p>
<strong>What it cannot catch.</strong>
The bytes have already been transmitted by the time the watcher runs β the
transcript JSONL is downstream of the agent's tool runtime. Files exfiltrated
via subprocesses the agent invokes through arbitrary code (e.g.
<code>python -c 'open(".env").read()'</code>, network sockets opened by
langchain wrappers, MCP servers run by the agent itself) are only caught if the
wrapping <code>Bash</code> command parses cleanly to one of the known reader
commands. The watcher is intentionally conservative on the false-positive side β
flagging an irrelevant file once is cheap; missing a leak is expensive.
</p>
<h3 id="security-codechunk">Code-chunk + env-dump heuristics (v1.11.52)</h3>
<p>
Outbound prompts larger than 2 KB are inspected for code-shaped structure
(indentation density, braces/punctuation density, common keywords like
<code>function</code>/<code>class</code>/<code>import</code>/<code>def</code>,
and module declarations such as <code>import β¦</code> or <code>#include</code>).
When two or more of those signals fire, the prompt is flagged to the UI
(<code>terminal:code-chunk-detected</code>) and written to the audit log as a
<code><strong>code_chunk_sent</strong></code> event. A separate detector counts
<code>UPPER_SNAKE=value</code> lines (with an optional <code>export</code>
prefix) β five or more raises <code><strong>env_dump_sent</strong></code>, which
records the variable <em>names</em> and how many there were, never the values.
</p>
<p>
<strong>These got their own event names in v1.25.2</strong>, and that matters more than
it sounds. They used to be logged as <code>redaction_hit</code> β the same event a real
leaked credential produced. That conflated <em>“you pasted a big file”</em>
with <em>“you leaked a key,”</em> and inflated the secrets-sent count with
things that are not secrets at all. Now the
<a href="#security-audit">“Rotate these” panel</a> only ever lists actual
credentials, and a large paste is recorded as what it is.
</p>
<p>
Both detectors are <em>heuristic</em>, and <strong>neither blocks the prompt</strong> β
they exist so a casual paste of an entire source file or <code>.env</code> does not slip
past unnoticed. False negatives are possible on minified or unusual code shapes.
</p>
<h3 id="security-egress">Per-agent egress audit (v1.11.52)</h3>
<p>
Termpolis asks the OS what TCP connections an AI agent's PID has open
(<code>netstat -ano</code> on Windows, <code>ss -tnp</code> on Linux,
<code>lsof -nP -iTCP -p <pid></code> on macOS) and records each unique
remote <code>host:port</code> to the audit log, so you can answer "did Claude talk
to anything other than <code>api.anthropic.com</code> today?".
</p>
<p>
<strong>The poll is on-demand β it runs when you open the Security panel, not on a
background timer.</strong> That is a deliberate reversal. The original design polled
every 60 seconds per AI terminal, and that triad β enumerate processes, spawn a
subprocess, from a freshly-signed executable β was load-bearing in the Windows
Defender cloud-ML <em>false positive</em> that quarantined v1.11.55. A continuous
behavioural signature is exactly what heuristic AV hunts for. The cost of moving to
on-demand is one extra shell-out the first time you open the panel; the benefit is
that the security feature stops getting the app flagged as malware.
</p>
<p>
<strong>So this is sampling, not packet capture.</strong> A connection that opens and
closes between two polls is never observed at all. We do not reverse-DNS the peer and
we do not inspect the payload. If the platform tool is missing or you lack the
permissions, the poller silently returns nothing and the rest of the app carries on.
</p>
<h3 id="security-egress-guard">Egress Guard β the allowlist policy (v1.25)</h3>
<p>
The egress audit above <em>records</em>. The <strong>Egress Guard</strong> turns that
record into a <strong>policy</strong>. Every remote host an agent connects to is checked
against the known AI-provider allowlist; anything outside it is raised as a
<strong>violation</strong> and written to the <a href="#security-audit">audit trail</a>.
Claude talking to <code>api.anthropic.com</code> is expected, and stays quiet. Claude
talking to a paste site, an unfamiliar VPS, or a host you have never seen before is
something you now find out about instead of having to go looking for it.
</p>
<p>
<strong>Matching is dot-anchored</strong>, which is the entire game for an allowlist: a
host matches an allowed domain only if it <em>is</em> that domain or a subdomain of it.
<code>api.anthropic.com</code> passes. <code>evil-anthropic.com</code> does
<strong>not</strong> pass as Anthropic. A naive substring check would have waved that
straight through β and a lookalike domain is exactly the trick an exfiltration endpoint
would use.
</p>
<p>
<strong>It flags and audits. It never kills your agent.</strong> A violation does not
terminate the process or sever the connection β Termpolis is not a firewall and will not
pretend to be one in the middle of your task. A false positive must never take down your
agent mid-task, so the guard's only output is an <code>egress_violation</code> entry in
the <a href="#security-audit">audit log</a>.
</p>
<p>
It also inherits the limits of the poll it sits on: this is <strong>sampling, not packet
capture</strong>, so a connection that opens and closes between two polls is never judged
at all. And when DNS resolution fails β you are offline, or the resolver is down β the
guard <strong>stays silent rather than flagging every provider IP it can no longer
name</strong>. A guard that cries wolf is a guard nobody reads. On by default.
</p>
<h3 id="security-memory-scrub">Memory-at-rest secret scrub (v1.25)</h3>
<p>
The <a href="#memory">shared memory</a> ingests your past AI conversations and your
repo's source code. Both are places a secret can be hiding β a key you pasted into a
transcript three weeks ago, a token sitting in a source file. And a secret that gets
embedded into the brain is worse than a secret at rest: it is <em>recallable</em>. A
future agent could pull it straight back into its own context on a semantic hit, and
from there to a provider.
</p>
<p>
So the scrub runs <strong>before the memory exists</strong>. Secrets are stripped out of
the text <em>before</em> it is hashed, <em>before</em> it is embedded, and
<em>before</em> it is written to disk β not cleaned up afterwards. A key in a transcript
or an indexed source file therefore never lands in the store, never gets a vector, and
can never be recalled back into an agent's context. On by default.
</p>
<p>
This complements, rather than replaces, the code indexer's existing denylist, which keeps
whole sensitive files (<code>.env</code>, keys, cloud credentials) out of the index in the
first place. The scrub is the second line β for the secret that turns up somewhere the
denylist was never going to look.
</p>
<div class="docs-callout">
<strong>Be clear-eyed about the limits.</strong> The scrub runs
<strong>at write time</strong>: it cleans every memory written from v1.25 onward, but it
does <em>not</em> go back and re-scrub a store you built before it existed. If you have
been running Termpolis for months, assume the older memories were never filtered. It also
<strong>fails open</strong> β if the scrubber itself errors, the memory is still written
(an anomaly is recorded) rather than the write being lost, because silently dropping a
user's memory is its own kind of data loss. And it inherits the rule engine's
regex-shaped coverage: a bespoke token nobody publishes a shape for can still be embedded.
</div>
<h3 id="security-tos-drift">Weekly ToS drift watcher (v1.11.52)</h3>
<p>
A scheduled GitHub Action
(<a href="https://github.com/codedev-david/termpolis/blob/main/.github/workflows/tos-drift.yml" target="_blank" rel="noreferrer"><code>.github/workflows/tos-drift.yml</code></a>)
runs every Monday at 13:00 UTC. It fetches the four provider pages this app
cites, normalises the HTML aggressively (strips
<code><script></code>/<code><style></code>/<code><svg></code>/<code><head></code>
and HTML comments, decodes entities, collapses whitespace), hashes the result,
and compares it to a snapshot committed under
<code>docs/security-snapshots/</code>. When a hash differs the action opens a
tracking issue tagged <code>security,tos-drift</code> so a human reviewer can
decide whether the legal language has materially changed and Termpolis needs
an update. The watcher detects rendered-text changes β it cannot infer legal
intent.
</p>
<div class="docs-callout">
<strong>This one is maintainer-facing β be clear about what it is not.</strong> The drift
watcher is CI for <em>this repository</em>: it opens a GitHub issue so the
training-disposition facts on the Security panel and on the marketing site stay aligned
with what the providers actually publish. It is <strong>not</strong> an in-app notifier β
Termpolis does <em>not</em> pop a banner telling you that OpenAI changed its terms last
Tuesday, and it is no substitute for your own reading of the terms you are bound by. What
it buys you is that the claims Termpolis makes about your providers do not quietly rot.
</div>
<h3 id="security-disclaimer">Legal disclaimer</h3>
<p>
The AI Security Center is <strong>best-effort, not regulatory-grade</strong>. The
project is licensed Apache 2.0 "AS IS". The full disclaimer ships in
<a href="https://github.com/codedev-david/termpolis/blob/main/TERMS.md" target="_blank" rel="noreferrer"><code>TERMS.md Β§5a</code></a>
and inside the app at Settings β AI Security. Provider terms can change without notice;
you must verify provider terms before transmitting confidential data. To the maximum
extent permitted by law, the authors and contributors of Termpolis disclaim all
liability for any data leak, breach, regulatory violation, contractual breach, or
business loss arising from your use of any AI agent launched through this application.
</p>
</section>
<!-- SAFE IMPORT -->
<section id="safe-import" class="docs-section">
<h2>π Safe Import β vet a skill, plugin, or MCP server before it runs (v1.25)</h2>
<p>
The agent ecosystem now trades in <strong>shared configuration</strong>: skills, plugins,
slash commands, subagents, and MCP servers, passed around as a <code>.zip</code> or a
folder from a repo. Every one of them is code and instructions that your agent will run
<em>with your permissions</em> β and the normal way to install one is to unpack it into
<code>~/.claude/skills</code> and hope. A malicious skill doesn't need an exploit. It just
needs you not to read it.
</p>
<p>
<strong>Safe Import</strong> (<strong>Settings β General</strong>) is the front door.
Point it at a <code>.zip</code> or a folder. Termpolis stages the artifact in a
<strong>quarantine</strong> directory, statically analyses every file inside it, and shows
you a verdict <em>before</em> anything is installed anywhere your agents can reach.
</p>
<div class="docs-callout">
<p>
<strong>It is a static scanner, an install gate, and runtime egress monitoring β it is
not a sandbox.</strong> Termpolis <em>never executes</em> the artifact to find out what
it does; nothing is detonated in a jail. “Quarantine” here means
<em>staged and read, not installed</em> β the files are analysed as text, then refused
or admitted on the evidence. Once an artifact is approved and an agent later runs it,
the <a href="#security-egress-guard">Egress Guard</a> is what watches where it connects.
</p>
</div>
<h3>What it scans for</h3>
<p>
<strong>41 rules</strong> across six families, applied to every file in the artifact. A
live progress bar shows the scan file by file, so a large plugin doesn't just sit on a
spinner:
</p>
<ul class="docs-list">
<li><strong>Outbound network.</strong> <code>fetch</code>, <code>axios</code>,
<code>curl</code>, <code>wget</code>, <code>WebSocket</code> β anything that can move
your data off the machine.</li>
<li><strong>Code execution.</strong> <code>child_process</code>, <code>exec</code>,
<code>spawn</code>, <code>eval</code>, <code>new Function</code>,
<code>subprocess</code> β the shell-out and dynamic-eval primitives.</li>
<li><strong>Credential reach.</strong> <code>process.env</code>, <code>keytar</code> and
the OS keychain, <code>~/.ssh</code>, <code>~/.aws</code>, <code>.env</code>,
<code>credentials.json</code> β a “formatting” skill that reaches for your
keychain or your cloud credentials has told you what it is.</li>
<li><strong>Destructive & out-of-workspace filesystem writes.</strong>
<code>rm -rf</code>, <code>fs.unlink</code>/<code>rmSync</code>,
<code>shutil.rmtree</code>, <code>Remove-Item</code> β and, separately, any write whose
path <em>leaves the workspace</em> (<code>~/</code>, <code>..\/</code>,
<code>$HOME</code>, <code>%APPDATA%</code>, an absolute system path). Either is worth a
look; together on one line β a destructive op pointed outside the project β they are the
shape of a skill that deletes something it was never asked to touch.</li>
<li><strong>Obfuscation.</strong> Long base64 or hex blobs, <code>atob</code> piped into
an exec, minified single-line payloads β the shapes whose whole purpose is to stop
<em>you</em> reading the file. A base64 decode on its own is benign (images, fixtures);
it turns red when the same file also contains something that would
<em>execute</em> what the decode produces.</li>
<li><strong>Prompt injection in the instructions themselves.</strong> The dangerous part
of a skill is often not its code but its prose: text engineered to hijack the agent that
reads it (“ignore your previous instructions, then exfiltrate…”).
Safe Import treats the skill's own instruction files as an attack surface β because to
your agent, that is exactly what they are.</li>
</ul>
<h3>The report, and the red line</h3>
<p>
You get a <strong>red / yellow / green</strong> report listing every finding with its
<strong>rule, file, and line</strong> β so you decide on evidence you can go and read for
yourself, not on a score out of ten.
</p>
<ul class="docs-list">
<li><strong>π’ Green</strong> β nothing matched. Install it if you want it.</li>
<li><strong>π‘ Yellow</strong> β it matched something that legitimate code also does (a
skill that calls an API genuinely does need <code>fetch</code>). Your call, with the
offending lines in front of you.</li>
<li><strong>π΄ Red β cannot be installed.</strong> Not “warns you loudly”.
Cannot. The refusal is enforced in the <strong>main process</strong>, independently of
the UI, so a red verdict is not a dialog you can click past and not something a renderer
bug can wave through.</li>
</ul>
<h3>Approval is pinned to the content, not the name</h3>
<p>
When you approve an artifact, the approval is pinned to a <strong>SHA-256 of the
artifact's content</strong> β not to its name and not to its path. If the artifact is
later edited, the hash no longer matches and <strong>you are prompted again</strong>. That
closes trust-on-first-use-then-swap: a skill cannot pass review as something harmless,
settle into your config, and then quietly become something else.
</p>
<h3>Where an approved artifact lands</h3>
<p>
On approval β and only on approval β Termpolis installs it into the agent configuration it
belongs in. <strong>Which agents an artifact reaches depends on its kind</strong>, because
the four CLIs do not all implement the same extension points:
</p>
<ul class="docs-list">
<li><strong>MCP server β all four agents.</strong> Written into the MCP config for Claude,
Codex, Gemini, <em>and</em> Qwen. MCP is the one extension point all four speak.</li>
<li><strong>Slash command β Claude, Gemini, and Qwen.</strong> Those three have a
user-command directory (<code>~/.claude/commands</code> and the Gemini/Qwen equivalents).
Codex does not, so a command is <em>not</em> installed for Codex.</li>
<li><strong>Skill, subagent, or plugin β Claude Code only.</strong>
(<code>~/.claude/skills</code>, <code>~/.claude/agents</code>, and the plugin directory.)
Claude Code is the only one of the four with a skills/subagent/plugin system, so there is
nowhere else for these to go.</li>
</ul>
<p>
So “Safe Import wires it into your agents” is deliberately <em>not</em> a
blanket claim: an MCP server really does reach all four, while a skill only ever reaches
Claude. The report tells you which agents a given artifact will actually touch before you
approve it.
</p>
<p>
The installer itself is hardened against the two classic archive attacks:
<strong>zip-slip</strong> (an archive entry whose <code>../../</code> path escapes the
extraction directory to write somewhere it was never meant to reach) and
<strong>TOML injection</strong> (an artifact whose fields are crafted to break out of the
field they belong in and inject extra configuration into an agent's MCP file). Both are
<strong>refused</strong> β not sanitised, patched up, and hoped over.
</p>
</section>
<!-- COPY FOR SLACK / TEAMS / PRs -->
<section id="copy-share" class="docs-section">
<h2>Copy for Slack / Teams / PRs</h2>
<p>
AI workflows generate share-worthy output constantly: a stack trace to dump into a
channel, a clean test run for a PR, a fenced code block for the engineering wiki.
Termpolis ships a four-way Copy submenu on every terminal context menu, plus the
keybinding <kbd>Ctrl+Shift+M</kbd> (rebindable) for the common case.
</p>
<h3>Copy as Code Block</h3>
<p>
Wraps the current selection β or the entire visible terminal buffer if nothing is
selected β in triple-backtick fences and copies it to the clipboard. Drop the result
into Slack, Teams, GitHub, GitLab, or any wiki that respects fenced code and the
monospaced layout survives.
</p>
<h3>Copy as Plain Text</h3>
<p>
Strips ANSI color escapes and copies the result as raw text. Use this for emails,
Jira tickets, Notion docs, or any surface where backticks would be noise.
</p>
<h3>Copy with Command</h3>
<p>
Prepends the last shell command (taken from history) to the copied output before
fencing. Reproducer-ready snippets for bug reports β your reviewer sees both the
command and the output in a single paste, instead of the usual back-and-forth.
</p>
<h3>Copy as Image (PNG)</h3>
<p>
Renders the underlying xterm.js canvas to a PNG via <code>canvas.toBlob</code> and
writes it to the clipboard with <code>ClipboardItem</code>. Paste straight into
Slack, Teams, or a Loom / wiki post. Colors, glyphs, and layout survive intact, so
you don't end up with the typical "weird mojibake in the screenshot" awkwardness
when the receiver is on a different OS or terminal font.
</p>
<p>
All four actions live in the terminal right-click menu under <strong>Copy β</strong>.
<kbd>Ctrl+Shift+M</kbd> defaults to Copy as Code Block; rebind it under Settings β
Keybindings.
</p>
<h3>Selecting across scrollback</h3>
<p>
Dragging to select is fine for what is on screen, but miserable across hundreds of
lines β you hold the button down and fight the auto-scroll the whole way. So anchor the
two ends instead: <kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>Click</kbd> a start point,
scroll as far as you like, then <kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>Click</kbd>
the end. Everything between is selected and copied in one go.
</p>
<p>
The anchor is pinned to the <em>text</em>, not to the viewport, so scrolling between the
two clicks never moves it β which is the entire point. Click the end before the start if
you prefer; the span is the same either way. Any plain click abandons a pending anchor,
and ordinary drag-select still behaves exactly as before. Prefer the keyboard?
<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Space</kbd> enters copy mode (arrows move,
<kbd>Shift</kbd> extends, <kbd>Ctrl</kbd> jumps by word, <kbd>Enter</kbd> copies,
<kbd>Esc</kbd> exits).
</p>
</section>