--- title: Configuration description: Turn inspectors, agent tools and panel actions on or off, add secret names and change what the devtools keep. --- One set of options for the Express hub, the Vite plugin and a custom devframe host. Turn inspectors, agent tools and actions off, add secret names and change the limits. # Configuration Everything is on when you leave the options out. The one exception is the Analog, Capacitor and TanStack Query agent tools, which are listed only for apps that use those libraries (see [library tools](../agents/tools.md#analog-capacitor-and-tanstack-query-tools)). You set them once, on the server. The server enforces them, and the overlay and the panel read them from the connection info. ## Where to set them These three functions take the same options: | Function | Import | Setup | | ------------------- | --------------------------------------- | --------------------------------------------------------------- | | `initPangularHub()` | `@pangular-inspector/devtools/hub` | [Angular CLI and Express](./express.md) | | `pangular()` | `@pangular-inspector/devtools/vite` | [Vite and Analog](./vite.md) | | `createPangular()` | `@pangular-inspector/devtools/devframe` | A custom devframe host, such as `initDevframe()` without a hub. | The `pangular` binary reads the same options from a JSON file, for `dev`, `build` and `mcp`. See [Config file](./cli.md#config-file). The access options stay with the server. The Express hub and the Vite plugin trust the [Chrome extension](./chrome-extension.md#server-origin) by its fixed ID, with no setting. A build of the extension with another ID needs its `chrome-extension://` origin in `allowedOrigins`. ### Express hub Pass the options next to the [access options](../security.md#express-hub) `auth`, `allowedOrigins` and `mcp`: ```ts {8-10} // src/server.ts import express from 'express'; import {initPangularHub} from '@pangular-inspector/devtools/hub'; const app = express(); const devtools = initPangularHub({ ws: false, inspectors: {pipes: false}, agent: {readOnly: true}, redaction: {secretNames: ['passport', 'taxId']}, }); app.use(devtools.nodeMiddleware); ``` ### Vite plugin Pass them to `pangular()`, next to the [access options](../security.md#vite-plugin) `auth` and `allowedOrigins`: ```ts {9-12} // vite.config.ts import analog from '@analogjs/platform'; import pangular from '@pangular-inspector/devtools/vite'; import {defineConfig} from 'vite'; export default defineConfig({ plugins: [ analog(), pangular({ actions: {http: false, analog: false}, limits: {refreshMs: 1000, navigations: 100}, }), ], }); ``` ### Custom devframe host The default export of `@pangular-inspector/devtools/devframe` uses the defaults. Call `createPangular()` to pass options: ```ts {6-9} // src/server.ts import express from 'express'; import {createPangular, initDevframe} from '@pangular-inspector/devtools/devframe'; const app = express(); const devtools = initDevframe(createPangular({agent: {readOnly: true}}), { base: '/__pangular/', ws: false, }); app.use(devtools.nodeMiddleware); ``` `ws: false` serves the connection over server-sent events on the same Express app. See [Mount only the panel](./express.md#mount-only-the-panel). ## The options ### Type `@pangular-inspector/devtools/config` exports the type. The hub and Vite entry points export it too. ```ts // @pangular-inspector/devtools/config type PangularInspector = | 'components' | 'injectors' | 'signals' | 'ngrx' | 'tanstackQuery' | 'forms' | 'router' | 'pipes' | 'http' | 'analog' | 'capacitor' | 'errors' | 'plugins'; type PangularAction = 'forms' | 'router' | 'ngrx' | 'http' | 'analog' | 'components' | 'tanstackQuery' | 'plugins'; interface PangularConfig { inspectors?: Partial>; agent?: { readOnly?: boolean; tools?: Partial>; browserTools?: boolean; }; actions?: boolean | Partial>; redaction?: { secretNames?: string[]; unmask?: string[]; entropy?: boolean | {minLength?: number; threshold?: number}; }; limits?: { refreshMs?: number; navigations?: number; formTimeline?: number; httpCalls?: number; changeLog?: number; cdCycles?: number; cdSlowMs?: number; capacitorCalls?: number; idleAfterMs?: number; }; popup?: {hotkey?: string | false}; plugins?: Record; lint?: {rules?: Record}; analog?: {contentSchema?: string}; ngrx?: {trace?: boolean}; } ``` ### Inspectors and agent tools | Option | Default | What it does | | -------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `inspectors.` | `true` | `false` removes the inspector: no tab or dock, no page collector, no RPC functions, and no agent tools or resources. Its actions are blocked too. | | `agent.readOnly` | `false` | `true` keeps only the agent tools in the `read` [safety tier](../agents/tools.md#action-tools). It drops every `action` and `destructive` tool in the [tier table](../agents/tools.md#action-tools): `run-form-action`, `fill-form`, `navigate`, `ngrx-dispatch`, `ngrx-restore`, `ngrx-store-devtools`, `tanstack-run-query`, `set-http-rule`, `remove-http-rule`, `set-ssr-override`, `remove-ssr-override`, `set-component-property`, `set-signal`, `load-defer-block`, `watch-signal`, `record-pipes`, `track-subscriptions`, `record-change-detection`, `probe-route`, `record-guards`, `analog-run-api`, `analog-run-nitro-task`, `analog-purge-nitro-cache` and `clear-recordings`. `audit` stays without its `axe` argument. | | `agent.tools.` | `true` | `false` hides one inspector's agent tools and resources, and its [shared-state keys](../agents/resources.md#shared-state) over HTTP MCP. The tab stays. | | `agent.browserTools` | `false` | `true` registers the `read` agent tools on the page for [browser agents](../agents/browser-agents.md) (WebMCP and Chrome DevTools for agents). Off by default, because any script on the development page could then call them. This is where the **Browser agent tools** switch in [Settings](./settings.md#browser-agents) starts. The switch changes it for every page until the dev server restarts. | The inspectors are `components`, `injectors`, `signals`, `ngrx`, `tanstackQuery`, `forms`, `router`, `pipes`, `http`, `analog`, `capacitor`, `errors`, `plugins`, `performance` ([Performance](../inspectors/performance.md)) and `build` ([Build](../inspectors/build.md)). `agent.tools` has the same keys as `inspectors`. An inspector that is off has no agent tools, whatever `agent.tools` says. If you open a tab that is turned off, the panel says so, shows the option that turns it back on, such as `inspectors: {ngrx: true}`, and links to this section. With `inspectors.components` set to `false`, the [Chrome extension](./chrome-extension.md) does not follow the **Elements** panel. ### Actions Actions are the writes that the panel and agents make to your app or the server. | Option | Default | What `false` blocks | | ----------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `actions.forms` | `true` | Form writes: set value, fill, mark touched, untouched, dirty or pristine, touch all, revalidate, reset, enable, disable, submit and restore. Drops `run-form-action` and `fill-form`. | | `actions.router` | `true` | Navigate, abort, replay and probe, and clearing the navigations list. Drops the `navigate` and `probe-route` agent tools and router clearing in `clear-recordings`. `record-guards` and `list-routes` with `resolveLazy` stay. | | `actions.ngrx` | `true` | Restoring NgRx state from the change log, dispatching `@ngrx/store` actions and changing the Store DevTools history. Drops the `ngrx-dispatch`, `ngrx-restore` and `ngrx-store-devtools` agent tools. | | `actions.http` | `true` | Editing fault injection rules and SSR overrides, and clearing the HTTP timeline. Drops the `set-http-rule`, `remove-http-rule`, `set-ssr-override` and `remove-ssr-override` agent tools and HTTP clearing in `clear-recordings`. | | `actions.analog` | `true` | The Analog request playground, clearing server calls, running Nitro tasks and purging the Nitro cache. Drops the `analog-run-api`, `analog-run-nitro-task` and `analog-purge-nitro-cache` agent tools and Analog clearing in `clear-recordings`. | | `actions.components` | `true` | Editing component inputs, properties and writable signals from the **Components** tab, and **Restore** in the **Signals** value history. Drops the `set-component-property`, `set-signal` and `load-defer-block` agent tools. | | `actions.tanstackQuery` | `true` | Invalidating and refetching a query from the **Queries** tab. Drops the `tanstack-run-query` agent tool. | | `actions.plugins` | `true` | Every plugin's page actions, from the **Plugins** tab and from its tools. Drops the plugin tools that are not `read`. | | `actions` | `true` | All of the above. | Pass `actions: false` to block every action, or an object to block some. Keys you leave out of the object stay on. `actions.forms` leaves the panel's **Record details** and **Store as global** on. They don't change the form: **Record details** wraps form methods to record who called them, and **Store as global** sets `$form` and `$control` in the page console only when the page has not defined them itself. The server refuses a blocked action, and with `actions.components` the page refuses it too. The panel disables its controls (the **Components** tab hides **Edit**) and shows a note that names the option, for example: **Navigating and clearing navigations is turned off in the devtools config (actions.router).** ### Redaction | Option | Default | What it does | | ----------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `redaction.secretNames` | `[]` | Extra field names to treat as secret, on top of the built-in list. They match by words, like the built-in list, so `passport` also covers `passportNumber`. | | `redaction.unmask` | `[]` | Field names to show even when they look secret. They join the `unmask` list of `window.__PANGULAR_FORMS__`. | | `redaction.entropy` | `false` | Also mask high-entropy strings that no name or shape rule catches, such as an opaque token in a harmless-looking field. Hex values and ULIDs stay readable. `true` turns it on with the defaults; an object turns it on and sets the options below. | | `redaction.entropy` field | Default | What it sets | | ------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `minLength` | `24` | The shortest run that can be masked, a whole number from 16 to 512. Raise it if ids in your app are masked that shouldn't be. | | `threshold` | `4.5` | The most bits per character a run needs, from 2 to 6. Shorter runs need less (see [High-entropy strings](../security.md#high-entropy-strings)). | A field that is out of range takes its default, and the server reports the problem when it starts. To mask only longer runs: ```ts // vite.config.ts import pangular from '@pangular-inspector/devtools/vite'; import {defineConfig} from 'vite'; export default defineConfig({ plugins: [pangular({redaction: {entropy: {minLength: 32}}})], }); ``` Forms, the router, component inputs, signals, NgRx, pipes, Analog previews, SSR & HTTP URLs and Capacitor plugin calls use the extra secret names. Each list keeps up to 100 names. `redaction.entropy` applies to the same text, on the page and again on the devtools server; [High-entropy strings](../security.md#high-entropy-strings) says what it masks. The server sends the `redaction` options to a page only after the page is trusted, so they are not in `__connection.json`. See [Access and redaction](../security.md#what-is-redacted) for what is redacted and what unmasking allows. ### Limits Values outside the bounds are clamped to the nearest one. | Option | Default | Bounds | What it sets | | ----------------------- | ------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `limits.refreshMs` | `3000` | 500 to 8000 | Fallback poll interval, in ms, used before the app bootstraps. | | `limits.navigations` | `50` | 5 to 500 | Navigations kept per page. | | `limits.formTimeline` | `200` | 10 to 2000 | Form timeline events kept. | | `limits.httpCalls` | `200` | 10 to 2000 | HTTP calls kept per page and on the server. | | `limits.changeLog` | `200` | 10 to 2000 | NgRx change log entries kept per page, and the actions kept per page for [other stores](../inspectors/ngrx-store.md#other-stores). | | `limits.cdCycles` | `200` | 10 to 2000 | Change detection cycles kept per page while recording. | | `limits.cdSlowMs` | `16` | 1 to 1000 | A [change detection](../inspectors/components.md#change-detection) cycle longer than this, in ms, counts as slow. | | `limits.capacitorCalls` | `200` | 10 to 2000 | [Capacitor](../inspectors/capacitor.md) plugin calls kept per page and on the server. | | `limits.idleAfterMs` | `60000` | 0 to 600000 | How long pages keep sending after an agent's last call while no panel is open. `0` keeps them sending. | When a timeline reaches its limit, the oldest entries are dropped. The timeline then shows a note above the list, such as "Showing the latest 200 HTTP calls. 12 older entries were dropped." with the limit to raise. This applies to the **SSR & HTTP** timeline, the router navigation timeline, the forms timeline, the NgRx change log, the change detection recording and the Capacitor plugin calls. The `explain-navigation`, `list-form-changes` and `change-detection` agent tools add the same note, and the `pangular:ngrx-store` resource reports a `dropped` count per page. On Angular 20 and later, the page reads the app about 250 ms after Angular runs change detection, and again every 4 seconds in case it missed a change. `refreshMs` doesn't change that. The page polls every `refreshMs` instead when it can't follow change detection, such as until the app bootstraps. The Analog inspector also reads the page every `refreshMs`. At any value, the page sends a small heartbeat every 10 seconds instead of data that didn't change, so the server never drops a page that is still open. If the server lost the page (it restarted, or the page went quiet for more than 30 seconds), the page sends all of its data again. ### Performance The component tree, injectors, change detection recording and forms cost the most to read and send on a large app, so the page keeps them cheap: - After one full report, the page sends only what changed. If the server lost track (a restart, a lost message), it asks for the whole report again. - The page sends each of them at most once a second, also while you type in a form. Changes in between go out together in the next report, so the forms timeline keeps every event. A form action from the panel or an agent is sent at once. - The component tree is read again only when elements were added or removed, or every 4 seconds. - The page sends them only while someone watches: a panel is open (the popup, the hub or the Chrome extension), or an agent called a tool in the last `limits.idleAfterMs`. An idle page only sends its heartbeat. The forms timeline keeps recording meanwhile, and so do the errors and console calls of the [Errors](../inspectors/errors.md) tab, which the page sends on the same terms. When a panel opens or an agent calls a tool, the page sends what changed meanwhile, and the agent's call waits up to 1.5 seconds for it. An agent call made during that wait waits for the same reports. A tab that closed or stopped reporting is not waited for. In the panel, the component tree, the HTTP call table, and the NgRx change log and events render only the rows near view when they have more than 300. Rows out of view are not in the page, so the browser's find doesn't see them. Use the tab's search field to search every row. ### Popup | Option | Default | What it does | | -------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `popup.hotkey` | `Alt+Shift+P` | The shortcut that opens and closes the [popup](./popup-and-hub.md#keyboard-and-mouse) on your app's page. `false` turns it off. | Write the shortcut as modifiers and one key joined with `+`, such as `Mod+Shift+K` or `Ctrl+Alt+D`: | Part | Values | | --------- | --------------------------------------------------------------------------------------------------------------- | | Modifiers | `Mod` (Cmd on macOS, Ctrl elsewhere), `Ctrl`, `Meta` or `Cmd`, `Alt` or `Option`, `Shift` | | Key | One letter, one digit, or `F1` to `F12` | It needs at least one modifier other than `Shift`, so typing in your app never sets it off. Letters and digits match by key position, so an Option combination that types another character on macOS still works. A value that is not a shortcut prints a warning and keeps `Alt+Shift+P`. ```ts {6} // src/server.ts import {initPangularHub} from '@pangular-inspector/devtools/hub'; const devtools = initPangularHub({ ws: false, popup: {hotkey: 'Mod+Shift+K'}, }); ``` The overlay reads the option from the server when it connects, so the default works until then. With `false`, [passive mode](./settings.md#passive-mode) has no effect and the floating button always shows. ### Plugins `inspectors.plugins: false` turns every [Pangular plugin](../guides/write-a-plugin.md) off: the **Plugins** tab, the page plugins, the server plugins' tools and state. `plugins` turns one plugin off, by its id, or only part of it: | Option | Default | What `false` turns off | | ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `plugins.` | `true` | The plugin: the page doesn't run it, and its tools and state are not registered. | | `plugins..tools` | `true` | The plugin's agent tools, its shared state over HTTP MCP, and the plugin in `list-plugins` and `inspect-plugin` answers. The `pangular:plugins` state, which holds every plugin's view, is hidden from HTTP MCP too. Its view stays in the panel. | | `plugins..actions` | `true` | The plugin's page actions and its tools that are not `read`. | ```ts // src/server.ts const devtools = initPangularHub({ plugins: {travel: {actions: false}}, }); ``` The server plugins themselves go in `serverPlugins`, an option of the Express hub, the Vite plugin and `createPangular()`'s host options. It is not part of `PangularConfig`, so the CLI config file doesn't take it. See [Write a plugin](../guides/write-a-plugin.md#add-agent-tools). ### Terminals `terminals` is an option of the Express hub and the Vite plugin only, next to the access options. It is not part of `PangularConfig`, so `createPangular()` and the CLI config file don't take it. | Option | Default | What it does | | ----------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `terminals` | `false` | `true` adds a **Terminals** dock to the hub that runs the project's build and test commands. It stays off in production, with `allowedOrigins: false` and with `allowLan`. See [Terminals](./terminals.md). | It is off by default because it starts processes on your machine. A value other than `true` or `false` prints a warning and keeps it off. ### Extra devframes `extraDevframes` is an option of the Express hub and the Vite plugin only. It mounts other devframes on the hub that serves Pangular Inspector, so a team's own devtools or another library's devframe sit next to the Pangular docks without a second server. It takes the same input as `initHub({ devframes })` from `@devframes/hub`: devframe definitions, `{ devframe, dock }` entries to change a dock, promises and functions that return them. ```ts import {initPangularHub} from '@pangular-inspector/devtools/hub'; import {acmeDevframe} from '@acme/devtools'; const devtools = initPangularHub({extraDevframes: [acmeDevframe]}); ``` The extra devframes run behind the same origin check, request gate and one-time code as Pangular Inspector. An entry with the id `pangular` would replace Pangular Inspector, so it is dropped with a warning, and a value that is not an array is ignored with a warning. ### LAN address `lanAddress` is an option of the Vite plugin only, and `--lan-address` the matching flag of `pangular dev`. Like `terminals`, it is not part of `PangularConfig`, so `createPangular()` and the CLI config file don't take it. | Option | Default | What it does | | ------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `lanAddress` | `false` | `true` injects the dev server's devtools URL on this machine's first private IPv4 address, or its first network address when none is private, as `__PANGULAR_LAN_URL__`. An address such as `'192.168.1.20'` uses that one. Needs `allowLan` in effect. See [Inject the LAN address](./vite.md#inject-the-lan-address). | `false` keeps it off with no warning. `true` and a network IPv4 address turn it on. Any other value, such as a loopback address, `0.0.0.0` or a value that is not a boolean or an IPv4 address, prints a warning and keeps it off. The CLI flag only prints the URL and the define for your build. See [Inject the LAN address](./cli.md#inject-the-lan-address). ### Lint rules | Option | Default | What it does | | ------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------- | | `lint.rules.` | `true` | `false` turns off an individual lint rule across linters (template lint, pipe usage, routes, forms and Analog diagnostics). | ```json { "lint": { "rules": { "img-missing-alt": false, "track-by-index": false } } } ``` A rule set to `false` is left out of the panel, the **Audit** tab, the agent tools (`scan-templates`, `scan-pipe-usage`, `explain-pipe`, `lint-routes`, `lint-forms`, `analog-lint` and `audit`) and the CI export (`pangular lint`). Use the rule name a finding shows. The template rules are listed under [scan-templates](../agents/tools.md#scan-templates). ### Analog | Option | Default | What it does | | ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `analog.contentSchema` | none | A JSON Schema file, relative to the Analog project root, that the frontmatter of every markdown file under `src/content` must match. The **Content** tab, `analog-scan-content` and `analog-lint` report the files that don't. | ```json { "analog": { "contentSchema": "content.schema.json" } } ``` A file outside the project, a missing file or a file that is not JSON gives one `content-schema-unreadable` lint warning, and the frontmatter is then only checked for syntax. See [Content](../inspectors/analog.md#content) for the keywords the check reads. ### NgRx | Option | Default | What it does | | ------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ngrx.trace` | `false` | `true` records where each [NgRx](../inspectors/ngrx-store.md#change-log) change log entry came from: up to 8 app frames of the dispatch, method call, `patchState` or event, without Angular, NgRx, RxJS, zone.js or devtools frames. Origins and query strings are removed, and secrets are masked like other values. | ```json { "ngrx": {"trace": true} } ``` Taking a stack on every write costs time, so `ngrx.trace` is off by default. The frames show in the entry detail on the **NgRx Store** tab, in `ngrx-history` and in [`ngrx-explain-change`](../agents/tools.md#ngrx-explain-change). In a bundled build without source maps, they point into the bundle. ## Check the active configuration The server checks the options when it starts. An unknown key, a value of the wrong type or a limit outside its bounds prints one `[pangular]` warning in the terminal that lists each problem and the value used instead. An unknown key names the closest known key: ```text [pangular] The devtools config has 2 problems: - Unknown option `agent.readonly` was ignored. Did you mean `readOnly`? - `actions` should be true, false or an object, got the string "false". It was ignored, so every action is allowed. ``` A value that the server ignores falls back to its default, which is usually the open setting. Read the warning after you change the options, especially when they come from environment variables or a JavaScript file. The [Dashboard](../inspectors/dashboard.md#configuration-block) has a **Configuration** block. It lists the options that differ from the defaults, or says **Defaults** when nothing is changed. The panel reads the `redaction` rows from the server once it is trusted, since `__connection.json` leaves them out. A static report has no server to ask, so it shows no **Extra secret names** or **Unmasked names** rows. | Row | Lists | | ------------------------------- | -------------------------------------------------------------------------------------- | | **Inspectors off** | Inspectors set to `false`. | | **Agent** | **Read-only** when `agent.readOnly` is `true`. | | **Hidden from the agent** | Inspectors whose agent tools are hidden. | | **Blocked actions** | Actions set to `false`. | | **Extra secret names** | `redaction.secretNames`. | | **Unmasked names** | `redaction.unmask`. | | **Entropy masking** | **On** when `redaction.entropy` is on, with the options that differ from the defaults. | | **Limits** | Limits that differ from the defaults. | | **Popup shortcut** | `popup.hotkey`, or **Off** when it is `false`. | | **Plugins off** | Plugins set to `false` in `plugins`. | | **Plugin tools or actions off** | Plugins with `tools` or `actions` set to `false`. | | **Lint rules off** | Rules set to `false` in `lint.rules`. | ## Where to next